From 2f48b07783eebc1b360171853f0f2bd5eedae15d Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:04:51 +0200 Subject: [PATCH 01/82] Bumped version to 0.3.2-SNAPSHOT/0.4.3-SNAPSHOT --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c9f7d67..175c866 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ // Project related options. -extra["coreVersion"] = "0.4.2-rc" +extra["coreVersion"] = "0.4.3-SNAPSHOT" // Dependency related options. extra["lampVersion"] = "4.0.0-rc.17" @@ -9,7 +9,7 @@ extra["bstatsVersion"] = "3.2.1" subprojects { group = "cloud.gteam" - version = "0.3.1-rc" + version = "0.3.2-SNAPSHOT" repositories { mavenCentral() From d38486b068acfd29af99d573dfcb0f8e050f4c6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:14:03 +0000 Subject: [PATCH 02/82] Bump the dev-deps group with 2 updates Bumps the dev-deps group with 2 updates: [com.gradleup.shadow](https://github.com/GradleUp/shadow) and [gradle-wrapper](https://github.com/gradle/gradle). Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3 - [Release notes](https://github.com/GradleUp/shadow/releases) - [Commits](https://github.com/GradleUp/shadow/compare/9.4.2...9.4.3) Updates `gradle-wrapper` from 9.6.0 to 9.6.1 - [Release notes](https://github.com/gradle/gradle/releases) - [Commits](https://github.com/gradle/gradle/compare/v9.6.0...v9.6.1) --- updated-dependencies: - dependency-name: com.gradleup.shadow dependency-version: 9.4.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: gradle-wrapper dependency-version: 9.6.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-deps ... Signed-off-by: dependabot[bot] --- bungeecord/build.gradle.kts | 2 +- core/build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- paper/build.gradle.kts | 2 +- spigot/build.gradle.kts | 2 +- velocity/build.gradle.kts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bungeecord/build.gradle.kts b/bungeecord/build.gradle.kts index ff1078c..a7bb258 100644 --- a/bungeecord/build.gradle.kts +++ b/bungeecord/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.2" + id("com.gradleup.shadow") version "9.4.3" } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index b8cf10b..399f497 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,7 +3,7 @@ import org.gradle.kotlin.dsl.shadowJar plugins { `java-library` - id("com.gradleup.shadow") version "9.4.2" + id("com.gradleup.shadow") version "9.4.3" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b248d97..1e8d71c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts index 9699f1e..3741c65 100644 --- a/paper/build.gradle.kts +++ b/paper/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.2" + id("com.gradleup.shadow") version "9.4.3" } diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index fa1cf90..76ef978 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.2" + id("com.gradleup.shadow") version "9.4.3" } diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 685a2de..56c8277 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.2" + id("com.gradleup.shadow") version "9.4.3" } From 494b69088317825c053096d7ec70e355c5273d89 Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:04:11 +0200 Subject: [PATCH 03/82] Working on Bungeecord support --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 317620d..53569fc 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,14 @@ If you wish to get support, test or have any questions about CoralGate, make sur BungeeCord ❌ + ✅ + ✅ + ✅ + ✅ + ❌ + ✅ + ✅ + ✅ ❓ From 2ec7c8b7ed3b7c9000fd99343416659af17c392b Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Sat, 4 Jul 2026 19:11:35 +0200 Subject: [PATCH 04/82] WIP: client settings/brand check --- .../gteam/coralgate/config/ConfigManager.java | 6 +- .../gteam/coralgate/config/ConfigModel.java | 13 + .../coralgate/processor/NetworkProcessor.java | 273 +++++++++++++++--- core/src/main/resources/config.yml | 19 +- 4 files changed, 270 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java b/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java index 1103921..36cd46f 100644 --- a/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java +++ b/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java @@ -28,10 +28,11 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; public class ConfigManager { - private final String latestConfigVersion = "0.2.1"; + private final String latestConfigVersion = "0.2.2"; private final String configFileName; private final File configFile; @@ -81,7 +82,7 @@ private void mapFields() { if (this.document == null) return; - this.config.setConfigVersion(this.document.getString("version", "0.2.1")); + this.config.setConfigVersion(this.document.getString("version", this.latestConfigVersion)); this.config.setNormalPrefix(this.document.getString("prefixes.normal", "§b§lCoralGate §7» §r")); this.config.setWarningPrefix(this.document.getString("prefixes.warning", "§6§lCoralGate §7» §r")); @@ -96,6 +97,7 @@ private void mapFields() { this.config.setApiTriggerFieldValue(this.document.getString("api-settings.trigger-field-value", "true")); this.config.setIgnoreLocalAddresses(this.document.getBoolean("filter-settings.ignore-local-addresses", true)); + this.config.setAllowedClientBrands(this.document.getStringList("filter-settings.allowed-client-brands", Arrays.asList("vanilla", "fabric", "forge", "neoforge", "quilt", "lunarclient", "badlion", "feather", "labymod4", "cheatbreaker", "geyser"))); } diff --git a/core/src/main/java/cloud/gteam/coralgate/config/ConfigModel.java b/core/src/main/java/cloud/gteam/coralgate/config/ConfigModel.java index ad36eda..007499e 100644 --- a/core/src/main/java/cloud/gteam/coralgate/config/ConfigModel.java +++ b/core/src/main/java/cloud/gteam/coralgate/config/ConfigModel.java @@ -18,6 +18,9 @@ package cloud.gteam.coralgate.config; +import java.util.Arrays; +import java.util.List; + public class ConfigModel { private String version; @@ -52,6 +55,8 @@ private static class FilterSettings { private boolean ignoreLocalAddresses = true; + private List allowedClientBrands = Arrays.asList("vanilla", "fabric", "forge", "neoforge", "quilt", "lunarclient", "badlion", "feather", "labymod4", "cheatbreaker", "Geyser"); + } public void setConfigVersion(final String version) { @@ -150,4 +155,12 @@ public boolean isIgnoreLocalAddresses() { return this.filterSettings.ignoreLocalAddresses; } + public List getAllowedClientBrands() { + return this.filterSettings.allowedClientBrands; + } + + public void setAllowedClientBrands(final List allowedClientBrands) { + this.filterSettings.allowedClientBrands = allowedClientBrands; + } + } diff --git a/core/src/main/java/cloud/gteam/coralgate/processor/NetworkProcessor.java b/core/src/main/java/cloud/gteam/coralgate/processor/NetworkProcessor.java index 3680aca..c593205 100644 --- a/core/src/main/java/cloud/gteam/coralgate/processor/NetworkProcessor.java +++ b/core/src/main/java/cloud/gteam/coralgate/processor/NetworkProcessor.java @@ -23,13 +23,20 @@ import com.github.retrooper.packetevents.event.PacketListener; import com.github.retrooper.packetevents.event.PacketReceiveEvent; import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.event.UserDisconnectEvent; import com.github.retrooper.packetevents.manager.server.ServerVersion; import com.github.retrooper.packetevents.protocol.ConnectionState; import com.github.retrooper.packetevents.protocol.packettype.PacketType; import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.wrapper.configuration.client.WrapperConfigClientPluginMessage; +import com.github.retrooper.packetevents.wrapper.configuration.client.WrapperConfigClientSettings; import com.github.retrooper.packetevents.wrapper.handshaking.client.WrapperHandshakingClientHandshake; import com.github.retrooper.packetevents.wrapper.login.client.WrapperLoginClientLoginStart; +import com.github.retrooper.packetevents.wrapper.login.server.WrapperLoginServerDisconnect; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPluginMessage; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientSettings; import com.github.retrooper.packetevents.wrapper.status.client.WrapperStatusClientPing; import com.github.retrooper.packetevents.wrapper.status.server.WrapperStatusServerResponse; @@ -38,6 +45,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; public class NetworkProcessor implements PacketListener { @@ -58,21 +66,21 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) { // Exempt local IP addresses according to configuration file. if (this.corePlugin.getConfigManager().getConfig().isIgnoreLocalAddresses() && !this.corePlugin.isTestMode()) { - try { + final InetAddress inetAddress = inetSocketAddress.getAddress(); - final InetAddress inetAddress = InetAddress.getByName(ipAddress); - - if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) return; - - } catch (final IOException ignored) {} + if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) + return; } final PacketTypeCommon packetTypeCommon = packetReceiveEvent.getPacketType(); + final ClientVersion clientVersion = packetReceiveEvent.getUser().getClientVersion(); + + System.out.println("C->S | " + packetTypeCommon); /* - * SOURCE PORT FILTERING. - */ + * SOURCE PORT FILTERING. + */ /* Check different condition to trigger a MOTD packet check and blockage. */ @@ -104,12 +112,17 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) { if (suspiciousPort) { // Log this suspicious connection. - if (!invalidPort) CorePlugin.getLogger().warning("Suspicious port used by client. Keep an eye out for " + inetSocketAddress + ". [C->S | " + packetTypeCommon.getName() + "]"); - else CorePlugin.getLogger().severe("Invalid port used by client. Closing connection from " + inetSocketAddress + ". [C->S | " + packetTypeCommon.getName() + "]"); + if (!invalidPort) + CorePlugin.getLogger().warning("Suspicious port used by client. Keep an eye out for " + inetSocketAddress + ". [C->S | " + packetReceiveEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]"); + else + CorePlugin.getLogger().severe("Invalid port used by client. Closing connection from " + inetSocketAddress + ". [C->S | " + packetReceiveEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]"); // Report the IP to CoralGate API. this.corePlugin.getApiManager().reportIp(ipAddress); + // Clean up. + this.connectionState.remove(inetSocketAddress); + } if (processMOTD) { @@ -174,8 +187,8 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) { /* Source port is ok, check IP now. */ /* - * API IP CHECK. - */ + * API IP CHECK. + */ // Match every login sequence related packet. final boolean isLoginSequencePacket = packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE @@ -200,7 +213,7 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) { } /* - * PROTOCOL FILTERING. + * PROTOCOL FILTERING. */ if (isStatusPacket || isLoginSequencePacket) { @@ -210,7 +223,7 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) { if (!scannerTest) { // If the client's protocol version is invalid. - if (packetReceiveEvent.getUser().getClientVersion() == null || packetReceiveEvent.getUser().getClientVersion().getProtocolVersion() == -1) { + if (clientVersion == null || clientVersion.getProtocolVersion() == -1) { // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection. logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid protocol version."); @@ -224,9 +237,12 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) { } + // We don't need those packets anymore. + if (isStatusPacket) return; + /* - * PACKET ORDER FILTERING. - */ + * PACKET ORDER FILTERING. + */ // Specific 'LOGIN' handshake login, the first packet in a legitimate connection sequence. if (packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE) { @@ -300,11 +316,162 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) { } - final PacketTypeCommon expectedFinalState = PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_20_2) - ? PacketType.Login.Server.LOGIN_SUCCESS - : PacketType.Login.Client.LOGIN_SUCCESS_ACK; + // Prior to 1.20.2 CLIENT_SETTINGS are sent via the PLAY state right after LOGIN_SUCCESS. + // On 1.20.2+, CLIENT_SETTINGS is sent exclusively via the CONFIGURATION state. + // This is exclusively used on < 1.20.2. + // Ignore the packet if the connection is already done. This can be triggered in game. + if (packetTypeCommon == PacketType.Play.Client.CLIENT_SETTINGS && this.connectionState.getOrDefault(inetSocketAddress, null) != PacketType.Play.Client.PLUGIN_MESSAGE) { + + final PacketTypeCommon expectedState = clientVersion.isOlderThan(ClientVersion.V_1_20_2) + ? PacketType.Configuration.Client.CONFIGURATION_END_ACK + : PacketType.Login.Server.LOGIN_SUCCESS; + + // Validate state against certain conditions: previous LOGIN_SUCCESS (< 1.20.2), Configuration.PLUGIN_MESSAGE (>= 1.20.2). + // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. + verifyAndTransitionState(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, expectedState, packetTypeCommon, "Missing login success procedure before client settings."); + + final WrapperPlayClientSettings wrapperPlayClientSettings = new WrapperPlayClientSettings(packetReceiveEvent); + + // If view distance exceeds minimum or maximum (vanilla). + if (wrapperPlayClientSettings.getViewDistance() < 2 || wrapperPlayClientSettings.getViewDistance() > 32) + logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid view distance."); + + // TODO: maybe go deeper in registries or more checks regarding settings? + + // Block further logic. + return; + + } + + // Minecraft versions prior to 1.20.2 exclusively use PLUGIN_MESSAGE in the PLAY state. + // Versions from 1.20.2 and above first use PLUGIN_MESSAGE in the CONFIGURATION state to send the brand, + // then uses the PLAY state to send "minecraft:register". + if (packetTypeCommon == PacketType.Play.Client.PLUGIN_MESSAGE) { + + final WrapperPlayClientPluginMessage wrapperPlayClientPluginMessage = new WrapperPlayClientPluginMessage(packetReceiveEvent); + final String channelName = wrapperPlayClientPluginMessage.getChannelName(); + + // Calculate expected state since 1.20.2+ also uses the CONFIGURATION state. + // On < 1.20.2, PLUGIN_MESSAGE is sent twice in a row, once for "minecraft:brand" and another "minecraft:register". + final PacketTypeCommon expectedState = clientVersion.isOlderThan(ClientVersion.V_1_20_2) + ? channelName.equals("minecraft:register") + ? PacketType.Play.Client.PLUGIN_MESSAGE + : PacketType.Play.Client.CLIENT_SETTINGS + : PacketType.Configuration.Client.CONFIGURATION_END_ACK; + + // Validate state against certain conditions: previous Play.CLIENT_SETTINGS/Play.PLUGIN_MESSAGE (< 1.20.2), CONFIGURATION_END_ACK (>= 1.20.2). + // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. + verifyAndTransitionState(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, expectedState, packetTypeCommon, "Missing configuration before client brand."); + + // Took from TotemGuard: https://github.com/Bram1903/TotemGuard/blob/main/src/main/java/com/deathmotion/totemguard/checks/impl/misc/ClientBrand.java + // All credits to Bram! + // Check client brand. + if (!channelName.equals("minecraft:brand") && !channelName.equals("MC|BRAND")) return; + + final byte[] data = wrapperPlayClientPluginMessage.getData(); + + // Weird ahhhh brand, im not waisting my time on no namer brands. + if (data.length > 64 || data.length == 0) + logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid client brand. (" + data.length + " bytes)."); + + final byte[] minusLength = new byte[data.length - 1]; + System.arraycopy(data, 1, minusLength, 0, minusLength.length); + String clientBrand = new String(minusLength).replace(" (Velocity)", ""); // removes velocity's brand suffix + clientBrand = !clientBrand.isEmpty() ? Pattern.compile("(?i)" + '§' + "[0-9A-FK-ORX]").matcher(clientBrand).replaceAll("") : clientBrand; + + // Special handling for lunar... + if (clientBrand.startsWith("lunarclient:")) clientBrand = "lunarclient"; + + if (!this.corePlugin.getConfigManager().getConfig().getAllowedClientBrands().contains(clientBrand)) + logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Unauthorized client brand name (" + clientBrand + ")."); + + // Block further logic. + return; + + } + + // Used exclusively on 1.20.2+, the client brand is sent here. + if (packetTypeCommon == PacketType.Configuration.Client.PLUGIN_MESSAGE) { + + // Validate state against certain conditions: previous LOGIN_SUCCESS_ACK. + // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. + verifyAndTransitionState(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, PacketType.Configuration.Server.CONFIGURATION_END, packetTypeCommon, "Missing configuration end from server."); + + // Took from TotemGuard: https://github.com/Bram1903/TotemGuard/blob/main/src/main/java/com/deathmotion/totemguard/checks/impl/misc/ClientBrand.java + // All credits to Bram! + // Check client brand. + final WrapperConfigClientPluginMessage wrapperConfigClientPluginMessage = new WrapperConfigClientPluginMessage(packetReceiveEvent); + final String channelName = wrapperConfigClientPluginMessage.getChannelName(); + + if (!channelName.equals("minecraft:brand") && !channelName.equals("MC|BRAND")) return; + + final byte[] data = wrapperConfigClientPluginMessage.getData(); + + // Weird ahhhh brand, im not waisting my time on no namer brands. + if (data.length > 64 || data.length == 0) + logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid client brand. (" + data.length + " bytes)."); + + final byte[] minusLength = new byte[data.length - 1]; + System.arraycopy(data, 1, minusLength, 0, minusLength.length); + String clientBrand = new String(minusLength).replace(" (Velocity)", ""); // removes velocity's brand suffix + clientBrand = !clientBrand.isEmpty() ? Pattern.compile("(?i)" + '§' + "[0-9A-FK-ORX]").matcher(clientBrand).replaceAll("") : clientBrand; + + // Special handling for lunar... + if (clientBrand.startsWith("lunarclient:")) clientBrand = "lunarclient"; + + if (!this.corePlugin.getConfigManager().getConfig().getAllowedClientBrands().contains(clientBrand)) + logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Unauthorized client brand name (" + clientBrand + ")."); - if (this.connectionState.getOrDefault(inetSocketAddress, null) != expectedFinalState) + // Block further logic. + return; + + } + + // Used exclusively on 1.20.2+. + if (packetTypeCommon == PacketType.Configuration.Client.CLIENT_SETTINGS) { + + // Validate state against certain conditions: previous Configuration.PLUGIN_MESSAGE. + // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. + verifyAndTransitionState(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, PacketType.Configuration.Client.PLUGIN_MESSAGE, packetTypeCommon, "Missing client brand before client settings."); + + final WrapperConfigClientSettings wrapperConfigClientSettings = new WrapperConfigClientSettings(packetReceiveEvent); + + // If view distance exceeds minimum or maximum (vanilla). + if (wrapperConfigClientSettings.getViewDistance() < 2 || wrapperConfigClientSettings.getViewDistance() > 32) + logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid view distance."); + + // TODO: maybe go deeper in registries or more checks regarding settings? + + // Block further logic. + return; + + } + + // Once CLIENT_SETTINGS and PLUGIN_MESSAGE are validated by the server. + if (packetTypeCommon == PacketType.Configuration.Client.CONFIGURATION_END_ACK) { + + final PacketTypeCommon expectedState = clientVersion.isOlderThan(ClientVersion.V_1_20_2) + ? PacketType.Configuration.Server.CONFIGURATION_END + : PacketType.Configuration.Client.CLIENT_SETTINGS; + + // Validate state against certain conditions: previous Configuration.CLIENT_SETTINGS. + // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. + verifyAndTransitionState(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, expectedState, packetTypeCommon, "Missing full client settings and client brand before finishing configuration."); + + // Block further logic. + return; + + } + + // Get the proper login packet based off the client version. + final PacketTypeCommon currentState = this.connectionState.getOrDefault(inetSocketAddress, null); + + // Directly evaluate if the current state is valid for this version context. + final boolean isValidState = clientVersion.isOlderThan(ClientVersion.V_1_20_2) && PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2) + ? (currentState == PacketType.Configuration.Client.CONFIGURATION_END_ACK) || (currentState == PacketType.Play.Client.PLUGIN_MESSAGE) || (currentState == PacketType.Play.Client.CLIENT_SETTINGS) + : currentState == PacketType.Play.Client.PLUGIN_MESSAGE; + + if (!isValidState) logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Missing full connection procedure."); /* All checks passed! */ @@ -320,18 +487,16 @@ public void onPacketSend(final PacketSendEvent packetSendEvent) { // Exempt local IP addresses according to configuration file. if (this.corePlugin.getConfigManager().getConfig().isIgnoreLocalAddresses() && !this.corePlugin.isTestMode()) { - try { + final InetAddress inetAddress = inetSocketAddress.getAddress(); - final InetAddress inetAddress = InetAddress.getByName(ipAddress); - - if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) return; - - } catch (final IOException ignored) {} + if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) return; } final PacketTypeCommon packetTypeCommon = packetSendEvent.getPacketType(); + System.out.println("S->C | " + packetTypeCommon); + /* * PACKET ORDER FILTERING. */ @@ -343,7 +508,7 @@ public void onPacketSend(final PacketSendEvent packetSendEvent) { // Client should have passed login start procedure. if (packetTypeCommon == PacketType.Login.Server.ENCRYPTION_REQUEST && this.corePlugin.isOnlineMode()) { - // Validate state against certain conditions: previous LOGIN_START + // Validate state against certain conditions: previous LOGIN_START. // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. verifyAndTransitionState(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, PacketType.Login.Client.LOGIN_START, packetTypeCommon, "Missing login start procedure."); @@ -359,7 +524,7 @@ public void onPacketSend(final PacketSendEvent packetSendEvent) { ? PacketType.Login.Client.ENCRYPTION_RESPONSE : PacketType.Login.Client.LOGIN_START; - // Validate state against certain conditions: previous ENCRYPTION_RESPONSE (if online mode) else LOGIN_START + // Validate state against certain conditions: previous ENCRYPTION_RESPONSE (if online mode) else LOGIN_START. // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. verifyAndTransitionState(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, requiredPacketTypeCommon, packetTypeCommon, "Missing encryption response or login start procedure."); @@ -374,7 +539,7 @@ public void onPacketSend(final PacketSendEvent packetSendEvent) { ? (this.corePlugin.getCompressionThreshold() >= 0 ? PacketType.Login.Server.SET_COMPRESSION : PacketType.Login.Client.ENCRYPTION_RESPONSE) : (this.corePlugin.getCompressionThreshold() >= 0 ? PacketType.Login.Server.SET_COMPRESSION : PacketType.Login.Client.LOGIN_START); - // Validate state against certain conditions: previous SET_COMPRESSION (if above 0) else ENCRYPTION_RESPONSE (if online mode) else LOGIN_START + // Validate state against certain conditions: previous SET_COMPRESSION (if above 0) else ENCRYPTION_RESPONSE (if online mode) else LOGIN_START. // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. verifyAndTransitionState(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, requiredPacketTypeCommon, packetTypeCommon, "Missing set compression procedure."); @@ -383,38 +548,70 @@ public void onPacketSend(final PacketSendEvent packetSendEvent) { } + // Sent after the server is done sending basic server data. + // Right before the client starts sending its own client data. + if (packetTypeCommon == PacketType.Configuration.Server.CONFIGURATION_END) { + + // Validate state against certain conditions: previous LOGIN_SUCCESS_ACK. + // If something is wrong, log the violation, report the IP to CoralGate API, cancel the packet and close the connection. + verifyAndTransitionState(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, PacketType.Login.Client.LOGIN_SUCCESS_ACK, packetTypeCommon, "Missing set compression procedure."); + + // Block further logic. + return; + + } + // Get the proper login packet based off the client version. - final PacketTypeCommon requiredPacketTypeCommon = packetSendEvent.getUser().getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_20_2) - ? PacketType.Login.Client.LOGIN_SUCCESS_ACK - : PacketType.Login.Server.LOGIN_SUCCESS; + final PacketTypeCommon currentState = this.connectionState.getOrDefault(inetSocketAddress, null); + + // Directly evaluate if the current state is valid for this version context. + final boolean isValidState = packetSendEvent.getUser().getClientVersion().isOlderThan(ClientVersion.V_1_20_2) && PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_20_2) + ? (currentState == PacketType.Login.Server.LOGIN_SUCCESS || currentState == PacketType.Play.Client.PLUGIN_MESSAGE) + : (currentState == PacketType.Login.Client.LOGIN_SUCCESS_ACK || currentState == PacketType.Configuration.Client.CONFIGURATION_END_ACK || currentState == PacketType.Play.Client.PLUGIN_MESSAGE); - // Connection procedure is not done yet, block outgoing packets. - if (this.connectionState.getOrDefault(inetSocketAddress, null) != requiredPacketTypeCommon) - packetSendEvent.setCancelled(true); + if (!isValidState) + logAndClose(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Missing full connection procedure."); /* All checks passed! */ } + @Override + public void onUserDisconnect(final UserDisconnectEvent userDisconnectEvent) { + + final User user = userDisconnectEvent.getUser(); + + if (user == null || user.getUUID() == null) return; + + // Clean up. + this.connectionState.remove(user.getAddress()); + + } + private void logAndClose(final PacketReceiveEvent packetReceiveEvent, final InetSocketAddress inetSocketAddress, final String ipAddress, final PacketTypeCommon packetTypeCommon, final String reason) { - CorePlugin.getLogger().severe(reason + " Closing connection from " + inetSocketAddress + ". [C->S | " + packetTypeCommon.getName() + "]"); + CorePlugin.getLogger().severe(reason + " Closing connection from " + inetSocketAddress + ". [C->S | " + packetReceiveEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]"); this.corePlugin.getApiManager().reportIp(ipAddress); packetReceiveEvent.setCancelled(true); packetReceiveEvent.getUser().closeConnection(); + // Clean up. + this.connectionState.remove(inetSocketAddress); + } private void logAndClose(final PacketSendEvent packetSendEvent, final InetSocketAddress inetSocketAddress, final String ipAddress, final PacketTypeCommon packetTypeCommon, final String reason) { - CorePlugin.getLogger().severe(reason + " Closing connection from " + inetSocketAddress + ". [S->C | " + packetTypeCommon.getName() + "]"); - + CorePlugin.getLogger().severe(reason + " Closing connection from " + inetSocketAddress + ". [S->C | " + packetSendEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]"); this.corePlugin.getApiManager().reportIp(ipAddress); packetSendEvent.setCancelled(true); packetSendEvent.getUser().closeConnection(); + // Clean up. + this.connectionState.remove(inetSocketAddress); + } private void verifyAndTransitionState(final PacketReceiveEvent packetReceiveEvent, final InetSocketAddress inetSocketAddress, final String ipAddress, final PacketTypeCommon packetTypeCommon, final PacketTypeCommon requiredState, final PacketTypeCommon targetState, final String reason) { diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 3b383ca..25fd448 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -6,7 +6,7 @@ # You need to restart your server in order to fully apply changes! # Config file version. DO NOT CHANGE THIS! -version: "0.2.1" +version: "0.2.2" # Per category message prefixes. prefixes: @@ -87,3 +87,20 @@ filter-settings: # Expected value type: Boolean # Expected value: true ignore-local-addresses: true + + # Client brands which are allowed to connect. + # This contains the big majority of widely used clients. + # If any is missing, feel free to add it. + # If the client brand you want to allow doesn't send a proper name, feel free to disable the client brand check. + allowed-client-brands: + - vanilla + - fabric + - forge + - neoforge + - quilt + - lunarclient + - badlion + - feather + - labymod4 + - cheatbreaker + - geyser \ No newline at end of file From 60416ed7118c7ca369146184812b40a192f4f288 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:15:04 +0000 Subject: [PATCH 05/82] Bump com.gradleup.shadow from 9.4.3 to 9.5.1 in the dev-deps group Bumps the dev-deps group with 1 update: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1 - [Release notes](https://github.com/GradleUp/shadow/releases) - [Commits](https://github.com/GradleUp/shadow/compare/9.4.3...9.5.1) --- updated-dependencies: - dependency-name: com.gradleup.shadow dependency-version: 9.5.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-deps ... Signed-off-by: dependabot[bot] --- bungeecord/build.gradle.kts | 2 +- core/build.gradle.kts | 2 +- paper/build.gradle.kts | 2 +- spigot/build.gradle.kts | 2 +- velocity/build.gradle.kts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bungeecord/build.gradle.kts b/bungeecord/build.gradle.kts index a7bb258..12b1d36 100644 --- a/bungeecord/build.gradle.kts +++ b/bungeecord/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.3" + id("com.gradleup.shadow") version "9.5.1" } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 399f497..692d3fb 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,7 +3,7 @@ import org.gradle.kotlin.dsl.shadowJar plugins { `java-library` - id("com.gradleup.shadow") version "9.4.3" + id("com.gradleup.shadow") version "9.5.1" } diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts index 3741c65..64d0a5d 100644 --- a/paper/build.gradle.kts +++ b/paper/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.3" + id("com.gradleup.shadow") version "9.5.1" } diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index 76ef978..24eff79 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.3" + id("com.gradleup.shadow") version "9.5.1" } diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 56c8277..4f3eb18 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("com.gradleup.shadow") version "9.4.3" + id("com.gradleup.shadow") version "9.5.1" } From f9f29b19cb424bd885c3f47b13f108dad0fd3079 Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:47:38 +0200 Subject: [PATCH 06/82] Made code a bit more readable --- core/build.gradle.kts | 2 +- .../cloud/gteam/coralgate/CorePlugin.java | 25 ++++++++++++++----- .../coralgate/commands/CoralGateCommand.java | 4 ++- .../permissions/PermissionFactory.java | 4 ++- .../gteam/coralgate/config/ConfigManager.java | 6 ++++- .../gteam/coralgate/update/UpdateChecker.java | 14 ++++++++--- .../gteam/coralgate/utils/ConfigUtils.java | 6 ++++- 7 files changed, 47 insertions(+), 14 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 399f497..3a1ac75 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -39,12 +39,12 @@ dependencies { // Dependencies. implementation("io.github.revxrsal:lamp.common:$lampVersion") implementation("blue.endless:jankson:1.2.3") - compileOnly("com.google.code.gson:gson:2.14.0") implementation("org.asynchttpclient:async-http-client:2.16.0") implementation("org.jetbrains:annotations:26.1.0") implementation("dev.dejvokep:boosted-yaml:1.3.7") compileOnly("com.github.retrooper:packetevents-api:$packetEventsVersion") + compileOnly("com.google.code.gson:gson:2.14.0") } diff --git a/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java b/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java index 58c0716..850819b 100644 --- a/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java +++ b/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java @@ -60,7 +60,9 @@ public void onEnable(final Logger logger, final File dataFolder, final boolean o // packetevents versions do not match. if (!peCoreVersion.equals(peServerVersion)) { logger.warning("packetevents version mismatch! You are using version '" + peServerVersion + "' but core module uses '" + peCoreVersion + "'! You may experience issues or bugs. Update CoralGate and packetevents to fix this issue."); - } else logger.info("Using packetevents version '" + peCoreVersion + "'..."); + } else { + logger.info("Using packetevents version '" + peCoreVersion + "'..."); + } this.testMode = new File(dataFolder, "test.mode").exists(); @@ -71,7 +73,9 @@ public void onEnable(final Logger logger, final File dataFolder, final boolean o final String latestConfigVersion = this.configManager.getLatestConfigVersion(); // Compare to internal configuration version to see if it's outdated. - if (!Objects.equals(latestConfigVersion, this.configManager.getConfig().getConfigVersion())) logger.warning("Please consider upgrading your configuration file to the latest version: '" + latestConfigVersion + "'."); + if (!Objects.equals(latestConfigVersion, this.configManager.getConfig().getConfigVersion())) { + logger.warning("Please consider upgrading your configuration file to the latest version: '" + latestConfigVersion + "'."); + } logger.info("Using configuration file version '" + this.configManager.getConfig().getConfigVersion() + "'."); @@ -93,9 +97,13 @@ public void onEnable(final Logger logger, final File dataFolder, final boolean o }); - } else logger.info("API health check skipped."); + } else { + logger.info("API health check skipped."); + } - } else logger.info("API loading skipped (disabled by config)."); + } else { + logger.info("API loading skipped (disabled by config)."); + } this.onlineMode = onlineMode; @@ -125,8 +133,13 @@ public void onEnable(final Logger logger, final File dataFolder, final boolean o public void onDisable() { - if (this.apiManager != null) this.apiManager.shutdown(); - if (this.updateChecker != null) this.updateChecker.shutdown(); + if (this.apiManager != null) { + this.apiManager.shutdown(); + } + + if (this.updateChecker != null) { + this.updateChecker.shutdown(); + } } diff --git a/core/src/main/java/cloud/gteam/coralgate/commands/CoralGateCommand.java b/core/src/main/java/cloud/gteam/coralgate/commands/CoralGateCommand.java index c5ec0d5..65d60b0 100644 --- a/core/src/main/java/cloud/gteam/coralgate/commands/CoralGateCommand.java +++ b/core/src/main/java/cloud/gteam/coralgate/commands/CoralGateCommand.java @@ -120,7 +120,9 @@ public void configReload(final CommandActor actor) { config = configManager.getConfig(); // Compare current config version and latest version and alert the user if necessary. - if (!Objects.equals(latestConfigVersion, config.getConfigVersion())) actor.sendRawMessage(config.getWarningPrefix() + "Please consider upgrading your configuration file to the latest version: '" + latestConfigVersion + "'. Your configuration file is at version '" + config.getConfigVersion() + "'."); + if (!Objects.equals(latestConfigVersion, config.getConfigVersion())) { + actor.sendRawMessage(config.getWarningPrefix() + "Please consider upgrading your configuration file to the latest version: '" + latestConfigVersion + "'. Your configuration file is at version '" + config.getConfigVersion() + "'."); + } actor.sendRawMessage(config.getNormalPrefix() + "Configuration file reloaded!"); diff --git a/core/src/main/java/cloud/gteam/coralgate/commands/permissions/PermissionFactory.java b/core/src/main/java/cloud/gteam/coralgate/commands/permissions/PermissionFactory.java index 8115b3c..07e9bae 100644 --- a/core/src/main/java/cloud/gteam/coralgate/commands/permissions/PermissionFactory.java +++ b/core/src/main/java/cloud/gteam/coralgate/commands/permissions/PermissionFactory.java @@ -38,7 +38,9 @@ public PermissionFactory(final PermissionChecker permissionChecker) { final cloud.gteam.coralgate.commands.permissions.CommandPermission ann = annotations.get(cloud.gteam.coralgate.commands.permissions.CommandPermission.class); - if (ann == null) return null; + if (ann == null) { + return null; + } return (actor) -> permissionChecker.hasPermission(actor, ann.value()); diff --git a/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java b/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java index 36cd46f..868923c 100644 --- a/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java +++ b/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java @@ -52,8 +52,10 @@ public void load() { final InputStream defaultStream = getClass().getClassLoader().getResourceAsStream(this.configFileName); if (defaultStream == null) { + CorePlugin.getLogger().severe("Could not find default resource file: " + this.configFileName); return; + } this.config = new ConfigModel(); @@ -109,7 +111,9 @@ public void save() { if (!this.configFile.getParentFile().mkdirs()) CorePlugin.getLogger().severe("Couldn't create data folders. Is the directory read-only? No error to display."); } - if (this.document != null) this.document.save(); + if (this.document != null) { + this.document.save(); + } } catch (final IOException e) { CorePlugin.getLogger().severe("Couldn't write data to " + this.configFileName + ". Is the directory read-only? See error:" + e.getMessage()); diff --git a/core/src/main/java/cloud/gteam/coralgate/update/UpdateChecker.java b/core/src/main/java/cloud/gteam/coralgate/update/UpdateChecker.java index 5fb3fad..50c61f0 100644 --- a/core/src/main/java/cloud/gteam/coralgate/update/UpdateChecker.java +++ b/core/src/main/java/cloud/gteam/coralgate/update/UpdateChecker.java @@ -54,10 +54,14 @@ public CompletableFuture isUpToDate() { final String currentVersion = this.corePlugin.getPlatformProperties().getProperty("platform-version"); // This is a dev/preview build, assume it's "up to date" to not show an out of date console message. - if (currentVersion.endsWith("-SNAPSHOT")) return CompletableFuture.completedFuture(true); + if (currentVersion.endsWith("-SNAPSHOT")) { + return CompletableFuture.completedFuture(true); + } // Use cache. - if (this.updateCheckFuture != null) return this.updateCheckFuture; + if (this.updateCheckFuture != null) { + return this.updateCheckFuture; + } this.updateCheckFuture = this.httpClient.prepareGet("https://api.github.com/repos/GTeamX/CoralGate/releases/latest") .setHeader("User-Agent", "CoralGate-UpdateChecker/" + currentVersion) @@ -106,7 +110,11 @@ public void shutdown() { } try { - if (!this.httpClient.isClosed()) this.httpClient.close(); + + if (!this.httpClient.isClosed()) { + this.httpClient.close(); + } + } catch (final IOException e) { CorePlugin.getLogger().severe("Error closing UpdateChecker client: " + e.getMessage()); } diff --git a/core/src/main/java/cloud/gteam/coralgate/utils/ConfigUtils.java b/core/src/main/java/cloud/gteam/coralgate/utils/ConfigUtils.java index c2c710e..219dafa 100644 --- a/core/src/main/java/cloud/gteam/coralgate/utils/ConfigUtils.java +++ b/core/src/main/java/cloud/gteam/coralgate/utils/ConfigUtils.java @@ -34,7 +34,11 @@ public static boolean isOnlineMode(final String fileName) { final List lines = Files.readAllLines(Paths.get(fileName)); for (final String line : lines) { - if (line.trim().startsWith("online-mode")) return line.contains("true"); + + if (line.trim().startsWith("online-mode")) { + return line.contains("true"); + } + } } catch (final IOException e) { From 670d09f784aab01149d4fe50d821a38ba12db080 Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:48:08 +0200 Subject: [PATCH 07/82] Switched API to synchronous fetching instead of async --- .../cloud/gteam/coralgate/api/APIManager.java | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/cloud/gteam/coralgate/api/APIManager.java b/core/src/main/java/cloud/gteam/coralgate/api/APIManager.java index 901bbb0..c135d91 100644 --- a/core/src/main/java/cloud/gteam/coralgate/api/APIManager.java +++ b/core/src/main/java/cloud/gteam/coralgate/api/APIManager.java @@ -61,21 +61,24 @@ public APIManager(final CorePlugin corePlugin) { this.jankson = Jankson.builder().build(); } - public CompletableFuture isIpBlocked(final String ipAddress) { + public boolean isIpBlocked(final String ipAddress) { // Get IP from cache before fetching from API. final CacheEntry entry = this.ipCache.get(ipAddress); // Serve cache if available. if (entry != null && !entry.isExpired(this.cacheTime)) { - return CompletableFuture.completedFuture(entry.isBlocked()); + return entry.isBlocked(); } - // Cache not available, fetch from API. - return fetchFromApi(ipAddress).thenApply(result -> { - if (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) this.ipCache.put(ipAddress, new CacheEntry(result)); - return result; - }); + // Cache not available, fetch from API synchronously. + boolean result = fetchFromApi(ipAddress); + + if (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) { + this.ipCache.put(ipAddress, new CacheEntry(result)); + } + + return result; } @@ -88,22 +91,26 @@ public void reportIp(final String ipAddress) { fetchFromApi(ipAddress); } - private CompletableFuture fetchFromApi(final String ipAddress) { + private boolean fetchFromApi(final String ipAddress) { - if (!this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) return CompletableFuture.completedFuture(false); + if (!this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) { + return false; // Not blocked. + } try { final InetAddress inetAddress = InetAddress.getByName(ipAddress); - - if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) return CompletableFuture.completedFuture(false); + if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) { + return false; // Not blocked. + } } catch (final UnknownHostException e) { CorePlugin.getLogger().severe("Couldn't parse IP address. Is the API properly configured? See error: " + e.getMessage()); this.healthStatus = false; - return CompletableFuture.completedFuture(false); + // Not blocked. + return false; } @@ -115,14 +122,14 @@ private CompletableFuture fetchFromApi(final String ipAddress) { .exceptionally(e -> { CorePlugin.getLogger().severe("Couldn't reach API. Is it down? See error: " + e.getMessage()); this.healthStatus = false; - return false; + return false; // Not blocked. }); // Keep track of requests to cleanly clear them on shutdown. this.pendingFutures.add(future); - future.whenComplete((res, ex) -> this.pendingFutures.remove(future)); + future.whenComplete((res, exception) -> this.pendingFutures.remove(future)); - return future; + return future.join(); } @@ -139,9 +146,12 @@ private boolean parseBlockedResponse(final Response response) { final JsonElement field = json.get(expectedField); if (field == null) { + CorePlugin.getLogger().severe("Couldn't find field '" + expectedField + "' in API's JSON response. Did the API change? No error to display."); this.healthStatus = false; - return false; + + return false; // Not blocked. + } // Compare values. @@ -153,14 +163,14 @@ private boolean parseBlockedResponse(final Response response) { this.healthStatus = true; - return Objects.equals(actualValue, expectedValue); + return Objects.equals(actualValue, expectedValue); // Blocked. } catch (final Exception e) { CorePlugin.getLogger().severe("Couldn't parse '" + expectedField + "' status from API. Did the API change? See error: " + e.getMessage()); this.healthStatus = false; - return false; + return false; // Not blocked. } @@ -184,7 +194,9 @@ public CompletableFuture checkHealth() { // Extract the "health" field. final JsonElement healthField = json.get("health"); - if (healthField == null) return false; + if (healthField == null) { + return false; + } // Clean the value and compare to "OK". this.healthStatus = "OK".equalsIgnoreCase(healthField.toJson(false, false).replace("\"", "")); @@ -211,13 +223,21 @@ public void shutdown() { // Forcefully cancel any HTTP callbacks still hanging around. for (final CompletableFuture forFuture : this.pendingFutures) { - if (!forFuture.isDone()) forFuture.cancel(true); + + if (!forFuture.isDone()) { + forFuture.cancel(true); + } + } this.pendingFutures.clear(); try { - if (!this.httpClient.isClosed()) this.httpClient.close(); + + if (!this.httpClient.isClosed()) { + this.httpClient.close(); + } + } catch (final IOException e) { CorePlugin.getLogger().severe("Couldn't close AsyncHttpClient. See error: " + e.getMessage()); } From 1d40461222a84c5d329bffd98e38c0a8399c7756 Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:48:15 +0200 Subject: [PATCH 08/82] Remade scanner --- scanner/src/Main.java | 678 ++++++++++++++++++++++++++++-------------- 1 file changed, 458 insertions(+), 220 deletions(-) diff --git a/scanner/src/Main.java b/scanner/src/Main.java index e246ef4..64d36b1 100644 --- a/scanner/src/Main.java +++ b/scanner/src/Main.java @@ -1,287 +1,525 @@ +// Written by Claude Sonnet 5. +// GTeam does not claim ownership or this code. +// Yes I was too lazy to write it all... + /* -* -* This code was written by Gemini 3.1 Pro and cleaned up by a human. -* GTeam does not claim ownership of this code. -* -* Yeah I was too lazy to write it all... -* + * CoralGateScanner + * ------------------------------------------------------------ + * A raw Minecraft-protocol test client built to exercise every + * branch of NetworkProcessor (CoralGate). It does NOT use + * packetevents - it speaks the wire protocol directly so it can + * freely forge handshakes, bad protocol versions, and control its + * own *source* port (which is what NetworkProcessor actually + * inspects via InetSocketAddress#getPort()). + * + * Usage: + * java Main [protocolVersion] [timeoutMs] + * + * Example: + * java Main 127.0.0.1 25565 776 3000 + * + * Exit code: 0 if every test's actual outcome matched the expected + * outcome, 1 if at least one test deviated (i.e. a real security + * failure such as leaking real server data when it should not have). + * + * IMPORTANT: run this only against servers you own/operate. Binding + * arbitrary local source ports and forging handshakes is exactly the + * kind of traffic pattern that gets IPs auto-reported by your own + * CoralGate API manager - expect your own IP to get flagged during + * this run, that's the point. */ -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.net.BindException; -import java.net.InetSocketAddress; -import java.net.Socket; - -public class Main { - - private static final String TARGET_IP = "127.0.0.1"; - private static final int TARGET_PORT = 25565; - private static final int PROTOCOL_VERSION = 776; // 26.2 - - private static final String FORGED_MOTD = "{\"description\":{\"text\":\"\",\"extra\":[\"A Minecraft Server\"]},\"players\":{\"max\":20,\"online\":0},\"version\":{\"name\":\"CraftBukkit 26.2\",\"protocol\":776},\"enforcesSecureChat\":true}"; - - private static boolean isExposed = false; // Added to track if the backend leaked. - - private static final String RESET = "\u001B[0m"; - private static final String RED = "\u001B[31m"; - private static final String GREEN = "\u001B[32m"; - - public static void main(String[] args) { - - final long startTime = System.currentTimeMillis(); - - System.out.println("Starting CoralGate filter tester v1.1.1"); - - // Broken handshake. - System.out.print(" (1) Broken handshake: "); - runTest(50000, "STATUS", out -> { - sendHandshake(out, 1, PROTOCOL_VERSION); - sendStatusRequest(out); - }); - - // Bot username. - System.out.print(" (2) Bot username: "); - runTest(50001, "LOGIN", out -> { - sendHandshake(out, 2, PROTOCOL_VERSION); - sendLoginStart(out, "Player12345"); - }); - - // Suspicious port. - System.out.print(" (3) Suspicious port: "); - runTest(40000, "STATUS", out -> { - sendHandshake(out, 1, PROTOCOL_VERSION); - sendStatusRequest(out); - }); - - // Invalid port. - System.out.print(" (4) Invalid port: "); - runTest(30000, "STATUS", out -> { - sendHandshake(out, 1, PROTOCOL_VERSION); - sendStatusRequest(out); - }); - - // Skip handshake. - System.out.print(" (5) Jump packet: "); - runTest(65535, "LOGIN", out -> sendLoginStart(out, "CoralGate")); - - // Invalid protocol. - System.out.print(" (6) Invalid protocol: "); - runTest(50003, "STATUS", out -> { - sendHandshake(out, 1, 100); - sendStatusRequest(out); - }); - - System.out.println("Scan finished in " + (System.currentTimeMillis() - startTime) + "ms."); +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class Main { + + // ---------------------------------------------------------------- + // ANSI colors + // ---------------------------------------------------------------- + private static final String RESET = "\u001B[0m"; + private static final String BOLD = "\u001B[1m"; + private static final String DIM = "\u001B[2m"; + private static final String RED = "\u001B[31m"; + private static final String GREEN = "\u001B[32m"; + private static final String YELLOW = "\u001B[33m"; + private static final String CYAN = "\u001B[36m"; + + // ---------------------------------------------------------------- + // Config + // ---------------------------------------------------------------- + private static String HOST; + private static int PORT; + private static int PROTOCOL; // "legit" protocol version to advertise + private static int TIMEOUT_MS = 3000; + + // Local source ports used to simulate each client class. + // >=49152 -> normal dynamic port (Windows/Mac range) -> "legit" + // 32768..49151 -> below Windows/Mac range, above Linux one -> "suspicious" + // <32768 -> below Linux dynamic range -> "invalid" + private static final int LEGIT_LOCAL_PORT = 51000; + private static final int SUSPICIOUS_LOCAL_PORT = 40000; + private static final int INVALID_LOCAL_PORT = 10000; + + // Fingerprint of NetworkProcessor#getForgedMOTD() + private static final String FORGED_MARKER_1 = "Paper 26.2"; + private static final String FORGED_MARKER_2 = "\"protocol\":776"; + private static final String FORGED_MARKER_3 = "\"enforcesSecureChat\":true"; + + private static int passCount = 0; + private static int failCount = 0; + + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.out.println("Usage: java CoralGateScanner [protocolVersion] [timeoutMs]"); + System.exit(2); + } + HOST = args[0]; + PORT = Integer.parseInt(args[1]); + PROTOCOL = args.length >= 3 ? Integer.parseInt(args[2]) : 776; + if (args.length >= 4) TIMEOUT_MS = Integer.parseInt(args[3]); + + banner(); + + runTest("T1 Legit connection (full status handshake)", Main::testLegitStatus); + runTest("T2 Suspicious source port (status request)", Main::testSuspiciousPortStatus); + runTest("T3 Invalid source port (status request)", Main::testInvalidPortStatus); + runTest("T4 Invalid protocol version (status request)", Main::testInvalidProtocolStatus); + runTest("T5 Missing handshake before status request", Main::testMissingHandshakeStatus); + runTest("T6 Invalid source port (login intent)", Main::testInvalidPortLogin); + runTest("T7 Suspicious port + bot-like username (login)", Main::testSuspiciousPortBotUsername); + runTest("T8 Suspicious port + normal username (login)", Main::testSuspiciousPortNormalUsername); + runTest("T9 Legacy server list ping (0xFE)", Main::testLegacyPing); + runTest("T10 Legit login handshake, bot-like username", Main::testLegitPortBotUsername); + + summary(); + System.exit(failCount == 0 ? 0 : 1); } - private static void runTest(final int sourcePort, final String expectedState, final PacketSender packetSender) { + // ---------------------------------------------------------------- + // Test cases + // ---------------------------------------------------------------- - try (final Socket socket = new Socket()) { + /** Fully legitimate client: dynamic port, correct protocol, proper handshake -> should receive REAL info. */ + private static Outcome testLegitStatus() throws IOException { + try (Socket s = connect(LEGIT_LOCAL_PORT)) { + sendPacket(s, 0x00, buildHandshake(PROTOCOL, HOST, PORT, 1)); + sendPacket(s, 0x00, new byte[0]); // Status Request - socket.setReuseAddress(true); - socket.bind(new InetSocketAddress(sourcePort)); - socket.connect(new InetSocketAddress(TARGET_IP, TARGET_PORT), 3000); + RawPacket resp = receivePacket(s, TIMEOUT_MS); + String json = readString(new ByteArrayInputStream(resp.data)); - final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream()); - final DataInputStream dataInputStream = new DataInputStream(socket.getInputStream()); + long pingPayload = 0x1234ABCDL; + sendPacket(s, 0x01, longBytes(pingPayload)); // Ping + RawPacket pong = receivePacket(s, TIMEOUT_MS); + long pongPayload = readLong(pong.data); - packetSender.send(dataOutputStream); - parseServerResponse(dataInputStream, expectedState); + boolean forged = isForgedMotd(json); + boolean pingOk = pongPayload == pingPayload; - } catch (final BindException e) { - System.out.println(RED + "(!) Cannot bind to port " + sourcePort + ". Socket still locked by OS allocation." + RESET); - } catch (final Exception e) { - System.out.println(RED + "(!) Connection error: " + e.getMessage() + RESET); + String info = describeMotd(json); + if (!forged && pingOk) { + return Outcome.pass("Real server data received as expected. " + info); + } + return Outcome.fail("Legit traffic was treated as suspicious! forgedMotd=" + forged + + " pingEchoOk=" + pingOk + " raw=" + truncate(json)); + } catch (IOException e) { + return Outcome.fail("Legit connection was unexpectedly blocked/closed: " + e); } - } - private static void parseServerResponse(final DataInputStream dataInputStream, final String state) { - - try { - - readVarInt(dataInputStream); - final int packetId = readVarInt(dataInputStream); - - if ("STATUS".equals(state)) { - - if (packetId == 0x00) { - - final String jsonMOTD = readString(dataInputStream); - - if (jsonMOTD.equals(FORGED_MOTD)) { - - System.out.println(GREEN + "PASSED! (filter active)" + RESET); - - } else { - - System.out.println(RED + "FAILED! (MOTD retrieved: " + jsonMOTD + ")" + RESET); - isExposed = true; - - } - - } else { - - System.out.println(RED + "(!) Received unexpected Status packet ID: 0x" + Integer.toHexString(packetId) + RESET); - isExposed = true; - - } - - } else if ("LOGIN".equals(state)) { - - if (packetId == 0x00) { - - System.out.println(RED + "FAILED! (server reached)" + RESET); - isExposed = true; - - } else if (packetId == 0x01) { - - System.out.println(RED + "FAILED! (server replied with encryption request)" + RESET); - isExposed = true; - - } else if (packetId == 0x02) { - - System.out.println(RED + "FAILED! (server replied with login success)" + RESET); - isExposed = true; - - } else { - - System.out.println(RED + "FAILED! (0x" + Integer.toHexString(packetId) + ")" + RESET); - isExposed = true; + /** Suspicious source port during a status request -> filter should return the forged MOTD. */ + private static Outcome testSuspiciousPortStatus() throws IOException { + return expectForgedStatus(SUSPICIOUS_LOCAL_PORT, "Suspicious-port status request"); + } - } + /** Invalid source port during a status request -> filter should also return the forged MOTD. */ + private static Outcome testInvalidPortStatus() throws IOException { + return expectForgedStatus(INVALID_LOCAL_PORT, "Invalid-port status request"); + } + /** Legit port, but garbage/negative protocol version -> filter should return the forged MOTD. */ + private static Outcome testInvalidProtocolStatus() throws IOException { + try (Socket s = connect(LEGIT_LOCAL_PORT)) { + sendPacket(s, 0x00, buildHandshake(-1, HOST, PORT, 1)); + sendPacket(s, 0x00, new byte[0]); + RawPacket resp = receivePacket(s, TIMEOUT_MS); + String json = readString(new ByteArrayInputStream(resp.data)); + if (isForgedMotd(json)) { + return Outcome.pass("Forged MOTD correctly returned for invalid protocol version."); } + return Outcome.fail("Real server info leaked despite invalid protocol version! raw=" + truncate(json)); + } catch (IOException e) { + // Some servers may just close on a garbage handshake before status - that's also an + // acceptable "no info leaked" outcome. + return Outcome.pass("Connection closed/blocked on invalid protocol version before info leaked (" + e + ")."); + } + } - } catch (final IOException e) { - - // If the server was already exposed by a previous test, an IOException just means the vanilla server crashed the socket. - if (isExposed) { - - System.out.println(RED + "FAILED! (protocol crash)" + RESET); - - } else { - - // If it hasn't leaked yet, an abrupt drop indicates active proxy mitigation. - System.out.println(GREEN + "PASSED! (zero bytes returned)" + RESET); + /** Skip the handshake entirely and jump straight to a status request. */ + private static Outcome testMissingHandshakeStatus() throws IOException { + try (Socket s = connect(LEGIT_LOCAL_PORT)) { + sendPacket(s, 0x00, new byte[0]); // Status Request with no prior HANDSHAKE + RawPacket resp = receivePacket(s, TIMEOUT_MS); + String json = readString(new ByteArrayInputStream(resp.data)); + if (isForgedMotd(json)) { + return Outcome.pass("Forged MOTD correctly returned for missing handshake."); } - + return Outcome.fail("Real server info leaked with no prior handshake! raw=" + truncate(json)); + } catch (IOException e) { + return Outcome.pass("Connection closed/blocked with no prior handshake before info leaked (" + e + ")."); } - } - interface PacketSender { - void send(final DataOutputStream dataOutputStream) throws IOException; + /** Invalid port + LOGIN intent -> handshake itself should trigger an immediate disconnect. */ + private static Outcome testInvalidPortLogin() throws IOException { + try (Socket s = connect(INVALID_LOCAL_PORT)) { + sendPacket(s, 0x00, buildHandshake(PROTOCOL, HOST, PORT, 2)); // LOGIN intent + RawPacket resp = tryReceive(s, TIMEOUT_MS); + if (resp == null) { + return Outcome.pass("Connection closed immediately after handshake, as expected."); + } + return Outcome.fail("Connection stayed open after invalid-port LOGIN handshake! got packet id=" + + resp.id + " data=" + truncate(bytesToHex(resp.data))); + } catch (IOException e) { + return Outcome.pass("Connection closed/reset immediately, as expected (" + e + ")."); + } } - private static void sendHandshake(final DataOutputStream dataOutputStream, final int nextState, final int protocolVersion) throws IOException { - - final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - final DataOutputStream handshakeOutputStream = new DataOutputStream(byteArrayOutputStream); - - handshakeOutputStream.writeByte(0x00); - - writeVarInt(handshakeOutputStream, protocolVersion); - writeString(handshakeOutputStream, Main.TARGET_IP); - - handshakeOutputStream.writeShort(Main.TARGET_PORT); - - writeVarInt(handshakeOutputStream, nextState); - writePacket(dataOutputStream, byteArrayOutputStream.toByteArray()); - + /** Suspicious port (warn-only) + a bot-like username ("Player...") -> should be kicked at LOGIN_START. */ + private static Outcome testSuspiciousPortBotUsername() throws IOException { + try (Socket s = connect(SUSPICIOUS_LOCAL_PORT)) { + sendPacket(s, 0x00, buildHandshake(PROTOCOL, HOST, PORT, 2)); + sendPacket(s, 0x00, buildLoginStart("Player" + System.currentTimeMillis() % 1000)); + RawPacket resp = tryReceive(s, TIMEOUT_MS); + if (resp == null) { + return Outcome.pass("Connection closed after bot-like username, as expected."); + } + // A disconnect packet before login success also counts as correctly caught. + return Outcome.fail("Bot-like username was not rejected! got packet id=" + resp.id + + " data=" + truncate(bytesToHex(resp.data))); + } catch (IOException e) { + return Outcome.pass("Connection closed/reset after bot-like username, as expected (" + e + ")."); + } } - private static void sendStatusRequest(final DataOutputStream dataOutputStream) throws IOException { - - final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - final DataOutputStream requestOutputStream = new DataOutputStream(byteArrayOutputStream); - - requestOutputStream.writeByte(0x00); - writePacket(dataOutputStream, byteArrayOutputStream.toByteArray()); - + /** Suspicious port + a normal-looking username -> port alone should only warn, not close. */ + private static Outcome testSuspiciousPortNormalUsername() throws IOException { + try (Socket s = connect(SUSPICIOUS_LOCAL_PORT)) { + sendPacket(s, 0x00, buildHandshake(PROTOCOL, HOST, PORT, 2)); + sendPacket(s, 0x00, buildLoginStart("ScannerUser")); + RawPacket resp = tryReceive(s, TIMEOUT_MS); + if (resp != null) { + return Outcome.pass("Login sequence continued past the suspicious-port check (got packet id=" + + resp.id + "). Note: any later disconnect is your own server logic (online-mode/" + + "whitelist/etc.), not necessarily CoralGate."); + } + return Outcome.fail("Connection was closed immediately on a normal username - suspicious port " + + "alone should only log a warning, not close the connection during LOGIN."); + } catch (IOException e) { + return Outcome.fail("Connection closed/reset on a normal username at a suspicious (not invalid) " + + "port - suspicious port should only warn during LOGIN, not disconnect (" + e + ")."); + } } - private static void sendLoginStart(final DataOutputStream dataOutputStream, final String username) throws IOException { - - final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - final DataOutputStream loginStartOutputStream = new DataOutputStream(byteArrayOutputStream); - - loginStartOutputStream.writeByte(0x00); + /** Legit port + LOGIN intent + bot-like username -> should still be kicked (username check is independent of port). */ + private static Outcome testLegitPortBotUsername() throws IOException { + try (Socket s = connect(LEGIT_LOCAL_PORT + 1)) { + sendPacket(s, 0x00, buildHandshake(PROTOCOL, HOST, PORT, 2)); + sendPacket(s, 0x00, buildLoginStart("Player_Bot")); + RawPacket resp = tryReceive(s, TIMEOUT_MS); + if (resp == null) { + return Outcome.pass("Connection closed after bot-like username on a legit port, as expected."); + } + return Outcome.fail("Bot-like username was NOT rejected even on a legit port! got packet id=" + + resp.id + " data=" + truncate(bytesToHex(resp.data))); + } catch (IOException e) { + return Outcome.pass("Connection closed/reset after bot-like username, as expected (" + e + ")."); + } + } - writeString(loginStartOutputStream, username); + /** Old-style (pre-Netty) 0xFE server list ping, sent with no modern handshake. */ + private static Outcome testLegacyPing() throws IOException { + try (Socket s = connect(LEGIT_LOCAL_PORT + 2)) { + OutputStream out = s.getOutputStream(); + out.write(0xFE); + out.write(0x01); + out.flush(); + + s.setSoTimeout(TIMEOUT_MS); + int first; + try { + first = s.getInputStream().read(); + } catch (SocketTimeoutException e) { + return Outcome.info("No response to legacy ping within " + TIMEOUT_MS + + "ms (server may silently drop legacy pings - verify manually)."); + } + if (first == -1) { + return Outcome.pass("Connection closed on legacy ping with no prior handshake, as expected."); + } + if (first == 0xFF) { + // Legacy disconnect/kick packet: short length (UTF-16BE chars) + UTF-16BE string + DataInputStream dis = new DataInputStream(s.getInputStream()); + int len = dis.readUnsignedShort(); + byte[] strBytes = new byte[len * 2]; + dis.readFully(strBytes); + String message = new String(strBytes, StandardCharsets.UTF_16BE); + boolean forged = isForgedMotd(message); + return forged + ? Outcome.pass("Legacy ping answered with forged data, as expected.") + : Outcome.info("Legacy ping answered with a 0xFF packet - manually verify it doesn't " + + "leak real info: " + truncate(message)); + } + return Outcome.info("Legacy ping got an unexpected first byte (0x" + + Integer.toHexString(first) + ") - likely a modern packet sent in reply to a legacy " + + "ping (protocol mismatch on the plugin side); verify manually."); + } catch (IOException e) { + return Outcome.pass("Connection closed/reset on legacy ping, as expected (" + e + ")."); + } + } - loginStartOutputStream.writeLong(0L); - loginStartOutputStream.writeLong(1L); + // ---------------------------------------------------------------- + // Shared helpers + // ---------------------------------------------------------------- + + private static Outcome expectForgedStatus(int localPort, String label) throws IOException { + try (Socket s = connect(localPort)) { + sendPacket(s, 0x00, buildHandshake(PROTOCOL, HOST, PORT, 1)); + sendPacket(s, 0x00, new byte[0]); + RawPacket resp = receivePacket(s, TIMEOUT_MS); + String json = readString(new ByteArrayInputStream(resp.data)); + if (isForgedMotd(json)) { + return Outcome.pass("Forged MOTD correctly returned. (" + label + ")"); + } + return Outcome.fail("Real server info leaked! raw=" + truncate(json)); + } catch (IOException e) { + // Filter closing the connection outright instead of forging is still "no leak" - acceptable. + return Outcome.pass("Connection closed/blocked before any info leaked (" + e + ")."); + } + } - writePacket(dataOutputStream, byteArrayOutputStream.toByteArray()); + private static Socket connect(int localPort) throws IOException { + Socket socket = new Socket(); + socket.setReuseAddress(true); + socket.bind(new InetSocketAddress(localPort)); + socket.connect(new InetSocketAddress(HOST, PORT), TIMEOUT_MS); + return socket; + } + private static boolean isForgedMotd(String text) { + return text != null + && text.contains(FORGED_MARKER_1) + && text.contains(FORGED_MARKER_2) + && text.contains(FORGED_MARKER_3); } - private static void writePacket(final DataOutputStream dataOutputStream, final byte[] data) throws IOException { + private static String describeMotd(String json) { + String version = extract(json, "\"version\"\\s*:\\s*\\{[^}]*\"name\"\\s*:\\s*\"([^\"]*)\""); + String protocol = extract(json, "\"version\"\\s*:\\s*\\{[^}]*\"protocol\"\\s*:\\s*(-?\\d+)"); + String online = extract(json, "\"players\"\\s*:\\s*\\{[^}]*\"online\"\\s*:\\s*(\\d+)"); + String max = extract(json, "\"players\"\\s*:\\s*\\{[^}]*\"max\"\\s*:\\s*(\\d+)"); + return "version=" + version + " protocol=" + protocol + " players=" + online + "/" + max; + } - writeVarInt(dataOutputStream, data.length); + private static String extract(String text, String regex) { + Matcher m = Pattern.compile(regex).matcher(text); + return m.find() ? m.group(1) : "?"; + } - dataOutputStream.write(data); - dataOutputStream.flush(); + private static String truncate(String s) { + if (s == null) return "null"; + return s.length() > 160 ? s.substring(0, 160) + "..." : s; + } + private static String bytesToHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) sb.append(String.format("%02x ", b)); + return sb.toString().trim(); } - private static void writeVarInt(final DataOutputStream dataOutputStream, int value) throws IOException { + // -- Minecraft protocol wire helpers -------------------------------- + private static void writeVarInt(OutputStream out, int value) throws IOException { while (true) { - if ((value & ~0x7F) == 0) { - - dataOutputStream.writeByte(value); + out.write(value); return; - } - - dataOutputStream.writeByte((value & 0x7F) | 0x80); + out.write((value & 0x7F) | 0x80); value >>>= 7; - } - } - private static int readVarInt(final DataInputStream dataInputStream) throws IOException { + private static int readVarInt(InputStream in) throws IOException { + int value = 0, position = 0, b; + while (true) { + b = in.read(); + if (b == -1) throw new EOFException("Stream closed while reading VarInt"); + value |= (b & 0x7F) << position; + if ((b & 0x80) == 0) break; + position += 7; + if (position >= 32) throw new IOException("VarInt too big"); + } + return value; + } - int numRead = 0, result = 0; - byte read; - do { + private static void writeString(OutputStream out, String s) throws IOException { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + writeVarInt(out, bytes.length); + out.write(bytes); + } - read = dataInputStream.readByte(); - result |= ((read & 0b01111111) << (7 * numRead)); + private static String readString(InputStream in) throws IOException { + int len = readVarInt(in); + byte[] bytes = new byte[len]; + int read = 0; + while (read < len) { + int r = in.read(bytes, read, len - read); + if (r == -1) throw new EOFException("Stream closed while reading String"); + read += r; + } + return new String(bytes, StandardCharsets.UTF_8); + } - if (numRead++ > 5) throw new RuntimeException("VarInt too big"); + private static byte[] longBytes(long value) { + byte[] b = new byte[8]; + for (int i = 7; i >= 0; i--) { + b[i] = (byte) (value & 0xFF); + value >>>= 8; + } + return b; + } - } while ((read & 0b10000000) != 0); + private static long readLong(byte[] data) { + long v = 0; + for (int i = 0; i < 8; i++) v = (v << 8) | (data[i] & 0xFF); + return v; + } - return result; + private static byte[] buildHandshake(int protocol, String host, int port, int nextState) throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + writeVarInt(b, protocol); + writeString(b, host); + b.write((port >> 8) & 0xFF); + b.write(port & 0xFF); + writeVarInt(b, nextState); + return b.toByteArray(); + } + private static byte[] buildLoginStart(String username) throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + writeString(b, username); + // Zero UUID - covers modern protocol versions that expect a mandatory player UUID field. + // Harmless for older versions since NetworkProcessor only reads the username itself. + b.write(new byte[16]); + return b.toByteArray(); } - private static void writeString(final DataOutputStream dataOutputStream, final String value) throws IOException { + private static void sendPacket(Socket socket, int packetId, byte[] data) throws IOException { + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + writeVarInt(payload, packetId); + payload.write(data); + ByteArrayOutputStream full = new ByteArrayOutputStream(); + writeVarInt(full, payload.size()); + full.write(payload.toByteArray()); + OutputStream out = socket.getOutputStream(); + out.write(full.toByteArray()); + out.flush(); + } - final byte[] bytes = value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + private record RawPacket(int id, byte[] data) {} + + private static RawPacket receivePacket(Socket socket, int timeoutMs) throws IOException { + socket.setSoTimeout(timeoutMs); + InputStream in = socket.getInputStream(); + int length = readVarInt(in); + byte[] full = new byte[length]; + int read = 0; + while (read < length) { + int r = in.read(full, read, length - read); + if (r == -1) throw new EOFException("Connection closed mid-packet"); + read += r; + } + ByteArrayInputStream bais = new ByteArrayInputStream(full); + int id = readVarInt(bais); + byte[] data = bais.readAllBytes(); + return new RawPacket(id, data); + } - writeVarInt(dataOutputStream, bytes.length); + /** Like receivePacket but returns null instead of throwing on timeout/EOF (i.e. "connection did not respond"). */ + private static RawPacket tryReceive(Socket socket, int timeoutMs) throws IOException { + try { + return receivePacket(socket, timeoutMs); + } catch (SocketTimeoutException | EOFException e) { + return null; + } + } - dataOutputStream.write(bytes); + // ---------------------------------------------------------------- + // Test harness plumbing + // ---------------------------------------------------------------- + private interface TestCase { + Outcome run() throws IOException; } - private static String readString(final DataInputStream dataInputStream) throws IOException { + private record Outcome(Status status, String detail) { + static Outcome pass(String detail) { return new Outcome(Status.PASS, detail); } + static Outcome fail(String detail) { return new Outcome(Status.FAIL, detail); } + static Outcome info(String detail) { return new Outcome(Status.INFO, detail); } + } - final byte[] bytes = new byte[readVarInt(dataInputStream)]; + private enum Status { PASS, FAIL, INFO } - dataInputStream.readFully(bytes); + private static void runTest(String name, TestCase test) { + System.out.print(BOLD + name + RESET + " ".repeat(Math.max(1, 52 - name.length()))); + Outcome outcome; + try { + outcome = test.run(); + } catch (Exception e) { + outcome = Outcome.fail("Unhandled scanner exception: " + e); + } + switch (outcome.status()) { + case PASS -> { + System.out.println(GREEN + "[ PASS ]" + RESET); + passCount++; + } + case FAIL -> { + System.out.println(RED + "[ FAIL ]" + RESET); + failCount++; + } + case INFO -> System.out.println(YELLOW + "[ INFO ]" + RESET); + } + System.out.println(DIM + " " + outcome.detail() + RESET); + sleep(250); // let connectionState cleanup settle between tests + } - return new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + private static void sleep(long ms) { + try { Thread.sleep(ms); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } + } + private static void banner() { + System.out.println(CYAN + BOLD + "CoralGate NetworkProcessor Scanner" + RESET); + System.out.println(CYAN + "Target: " + HOST + ":" + PORT + " protocol=" + PROTOCOL + + " timeout=" + TIMEOUT_MS + "ms" + RESET); + System.out.println(DIM + "Local test ports -> legit>=49152: " + LEGIT_LOCAL_PORT + + " suspicious(32768-49151): " + SUSPICIOUS_LOCAL_PORT + + " invalid(<32768): " + INVALID_LOCAL_PORT + RESET); + System.out.println(); } -} + private static void summary() { + System.out.println(); + System.out.println(BOLD + "Summary: " + RESET + + GREEN + passCount + " passed" + RESET + ", " + + (failCount > 0 ? RED : DIM) + failCount + " failed" + RESET); + if (failCount > 0) { + System.out.println(RED + "One or more checks behaved unexpectedly - review the [ FAIL ] lines above." + RESET); + } else { + System.out.println(GREEN + "All checks behaved as expected." + RESET); + } + } +} \ No newline at end of file From 0882e811044e865d5eb6b8e7d76d9fe35216aae1 Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:49:18 +0200 Subject: [PATCH 09/82] Added platform type property --- bungeecord/src/main/resources/platform.properties | 1 + paper/src/main/resources/platform.properties | 1 + spigot/src/main/resources/platform.properties | 1 + velocity/src/main/resources/platform.properties | 1 + 4 files changed, 4 insertions(+) diff --git a/bungeecord/src/main/resources/platform.properties b/bungeecord/src/main/resources/platform.properties index 91d4b12..69b49b7 100644 --- a/bungeecord/src/main/resources/platform.properties +++ b/bungeecord/src/main/resources/platform.properties @@ -1,4 +1,5 @@ platform-name=${name} +platform-type=proxy platform-version=${version} core-version=${coreVersion} packetevents-version=${packeteventsVersion} \ No newline at end of file diff --git a/paper/src/main/resources/platform.properties b/paper/src/main/resources/platform.properties index 91d4b12..d255182 100644 --- a/paper/src/main/resources/platform.properties +++ b/paper/src/main/resources/platform.properties @@ -1,4 +1,5 @@ platform-name=${name} +platform-type=backend platform-version=${version} core-version=${coreVersion} packetevents-version=${packeteventsVersion} \ No newline at end of file diff --git a/spigot/src/main/resources/platform.properties b/spigot/src/main/resources/platform.properties index 91d4b12..d255182 100644 --- a/spigot/src/main/resources/platform.properties +++ b/spigot/src/main/resources/platform.properties @@ -1,4 +1,5 @@ platform-name=${name} +platform-type=backend platform-version=${version} core-version=${coreVersion} packetevents-version=${packeteventsVersion} \ No newline at end of file diff --git a/velocity/src/main/resources/platform.properties b/velocity/src/main/resources/platform.properties index 91d4b12..69b49b7 100644 --- a/velocity/src/main/resources/platform.properties +++ b/velocity/src/main/resources/platform.properties @@ -1,4 +1,5 @@ platform-name=${name} +platform-type=proxy platform-version=${version} core-version=${coreVersion} packetevents-version=${packeteventsVersion} \ No newline at end of file From 7ecdd4a9eda722e88173f1262040915d4af21604 Mon Sep 17 00:00:00 2001 From: XIII-MC <81795405+XIII-MC@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:49:48 +0200 Subject: [PATCH 10/82] Added run parameters for scanner --- .run/Scanner.run.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.run/Scanner.run.xml b/.run/Scanner.run.xml index ba9eecb..b6ae699 100644 --- a/.run/Scanner.run.xml +++ b/.run/Scanner.run.xml @@ -4,6 +4,7 @@