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 @@
+
diff --git a/README.md b/README.md
index 317620d..b587f6b 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
[//]: # (Main image, centered)
-
+
[//]: # (Main title, centered)
@@ -9,346 +9,143 @@
[//]: # (Shield.io badges, main basic stuff, centered)
On-the-fly packet inspection and real-time IP verification for Minecraft servers and networks.
-CoralGate acts as a high-performance application firewall for your Minecraft infrastructure. By analyzing incoming packets: malicious payloads, scanners and bots are blocked before they truly reach your server. It secures your network across every layer, from spoofing your MOTD to appear as a "no-namer" server, all the way to checking proper packet order and even checking certain incoming connection fields.
+CoralGate acts as a high-performance application firewall for your Minecraft infrastructure. By analyzing incoming packets: malicious payloads, scanners and bots are blocked before they truly reach your server. It secures your servers by spoofing your MOTD to appear as a generic server, verifies proper packet order and even checks certain incoming connection fields.
If you wish to get support, test or have any questions about CoralGate, make sure to join our [Discord server](https://discord.gteam.cloud)!
-## ✅ Supported platforms/versions
-
-
-
-
-
- < 1.8
- 1.8
- 1.8.3
- 1.8.8
- 1.9
- 1.9.1
- 1.9.2
- 1.9.3/4
- 1.10.x
- 1.11
- 1.11.x
- 1.12
- 1.12.1
- 1.12.2
- 1.13
- 1.13.1
- 1.13.2
- 1.14
- 1.14.1
- 1.14.2
- 1.14.3
- 1.14.4
- 1.15
- 1.15.1
- 1.15.2
- 1.16
- 1.16.1
- 1.16.2
- 1.16.3
- 1.16.4/5
- 1.17
- 1.17.1
- 1.18
- 1.18.1
- 1.18.2
- 1.19
- 1.19.1/2
- 1.19.3
- 1.19.4
- 1.20/.1
- 1.20.2
- 1.20.3/4
- 1.20.5/6
- 1.21/.1
- 1.21.2/3
- 1.21.4
- 1.21.5
- 1.21.6
- 1.21.7/8
- 1.21.9/10
- 1.21.11
- 26.1.x
- 26.2
- > 26.2
-
-
-
-
- Spigot
- ❌
- ⚠️
- ✅
- ✅
- ❌
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ❌
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ❌
- ✅
- ✅
- ❌
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ❓
-
-
- Paper
- ❌
- ✅
- ❌
- ✅
- ✅
- ❌
- ✅
- ❌
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ❌
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ❌
- ✅
- ✅
- ✅
- ✅
- ❌
- ✅
- ✅
- ❌
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ✅
- ❓
-
-
- BungeeCord
- ❌
- ❓
-
-
- Velocity
- ❌
- ❓
-
-
- Sponge
- ❌
-
-
- Fabric
- ❌
-
-
- Forge
- ❌
-
-
- NeoForge
- ❌
-
-
-
+## Supported Platforms
+
+CoralGate supports a wide range of Minecraft server implementations.
+
+| Platform | 1.8.x | 1.9.x – 1.15.x | 1.16.x – 1.20.x | 1.21.x | 26.x | > 26.2 |
+|----------------|:-----:|:--------------:|:---------------:|:------:|:----:|:---------:|
+| **Spigot** | ⚠️ | ✅ | ✅ | ✅ | ✅ | ❓ |
+| **Paper** | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❓ |
+| **BungeeCord** | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
+| **Velocity** | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
+| **Sponge** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Fabric** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Forge** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **NeoForge** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-📋 Versions notes
+View platform compatibility notes.
**Spigot**:
-- 1.8 throws an exception on any player join, not supported.
-- 1.8.3 works with an outdated packetevents version (2.10.1).
-- 1.9.1 jar cannot be downloaded or built.
-- 1.16 jar cannot be downloaded or built.
-- 1.19.3 throws java.lang.RuntimeException: The received byte array length is longer than maximum allowed (8742 > 512) for LOGIN_START wrapper.
-- 1.20.2 client sends PLUGIN_MESSAGE and CLIENT_SETTINGS after HANDSHAKE
+- **< 1.8.3:** not supported.
+- **1.8.3:** works with an outdated packetevents version (2.10.1).
+- **1.19.3:** not supported (broken server jar).
**Paper**:
-- Only 1.8.8 build exists.
-- Only 1.9.4 build exists.
-- Only 1.11.2 build exists.
-- Only 1.12.2 build works (S3 bucket unreachable for 1.12/.1).
-- Only 1.16.x build exists.
-- Only 1.18.x build exists.
-- 1.19.3 throws java.lang.RuntimeException: The received byte array length is longer than maximum allowed (8742 > 512) for LOGIN_START wrapper.
-- 1.20.2 client sends PLUGIN_MESSAGE and CLIENT_SETTINGS after HANDSHAKE
+- **1.19.3:** not supported (broken server jar).
+- **1.19.4 - 1.20.x:** not supported (broken modern 'paper-plugin.yml').
+
+*Sponge, Fabric, Forge, NeoForge and any hybrid server platform is not officially supported.*
-*Legend:*
-- ✅: *Fully supported.*
-- ⚠️: *Partially supported (missing features, using outdated dependencies, buggy or not fully tested...).*
-- ❓: *Unknown compatibility, not tested at all.*
-- ❌: *Not supported.*
+
+View fully detailed platform and version support list.
+
+| Platform | <1.8 | 1.8 | 1.8.3 | 1.8.8 | 1.9 | 1.9.1 | 1.9.2 | 1.9.3/4 | 1.10.x | 1.11 | 1.11.x | 1.12 | 1.12.1 | 1.12.2 | 1.13 | 1.13.1 | 1.13.2 | 1.14 | 1.14.1 | 1.14.2 | 1.14.3 | 1.14.4 | 1.15 | 1.15.1 | 1.15.2 | 1.16 | 1.16.1 | 1.16.2 | 1.16.3 | 1.16.4/5 | 1.17 | 1.17.1 | 1.18 | 1.18.1 | 1.18.2 | 1.19 | 1.19.1/2 | 1.19.3 | 1.19.4 | 1.20/.1 | 1.20.2 | 1.20.3/4 | 1.20.5/6 | 1.21/.1 | 1.21.2/3 | 1.21.4 | 1.21.5 | 1.21.6 | 1.21.7/8 | 1.21.9/10 | 1.21.11 | 26.1.x | 26.2 | >26.2 |
+|:---------------|:----:|:---:|:-----:|:-----:|:---:|:-----:|:-----:|:-------:|:------:|:----:|:------:|:----:|:------:|:------:|:----:|:------:|:------:|:----:|:------:|:------:|:------:|:------:|:----:|:------:|:------:|:----:|:------:|:------:|:------:|:--------:|:----:|:------:|:----:|:------:|:------:|:----:|:--------:|:------:|:------:|:-------:|:------:|:--------:|:--------:|:-------:|:--------:|:------:|:------:|:------:|:--------:|:---------:|:-------:|:------:|:----:|:--------:|
+| **Spigot** | ❌ | ❌ | ⚠️ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
+| **Paper** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
+| **BungeeCord** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
+| **Velocity** | ❌ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
+| **Sponge** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Fabric** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Forge** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **NeoForge** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+
+
-## ⬇️ Installation
+## Installation
-This installation procedure is platform independant, it is the same for every supported platform above:
-- Download the latest version of [packetevents](https://github.com/retrooper/packetevents/releases).
-- Download the latest version of CoralGate for your platform *(see download mirrors bellow)*.
-- Put both plugins in your "plugins" folder situated at the root of your server jar.
-- Restart your server.
+This installation procedure is platform independent, it is the same for every supported platform above:
+1. Download the latest version of [packetevents](https://github.com/retrooper/packetevents/releases).
+2. Download the latest version of CoralGate for your platform *(see download mirrors below)*.
+3. Put both plugins in your "plugins" folder situated at the root of your server jar.
+4. Restart your server.
CoralGate is now protecting your server from scanners!
-## 🔄 Updates
+## Download Mirrors
-Updates are released on a non-fixed schedule. Therefore update may appear at any given time, whereas to fix issues or add new features.
-The update process is the same as the installation process. To update your configuration file please take a look bellow for further instructions.
+To prevent any malware from infecting your server, only download CoralGate from our **trusted sources**:
+- [Official GitHub](https://github.com/GTeamX/CoralGate/releases)
-## ✏️ Configuration file
+## Configuration
-CoralGate has a very extensive and complete configuration file, situated in your plugins folder under the CoralGate folder. The file is named 'config.yml'.
-Every default value is marked in the comments above the field with explanation on it's impact.
+CoralGate has a very extensive and complete configuration file, situated in your plugins folder under the CoralGate folder. The file is named `config.yml`.
+Every default value is marked in the comments above the field with explanation on its impact.
If you ever mess up your configuration file, delete it and restart your server. The default configuration will be loaded.
-If you recently updated CoralGate and get a warning saying your configuration file is outdated, delete your configuration file and let the new default configuration appear. To ensure you loose none of your settings, we advise you make a backup of your config.yml file beforehand.
-
-## 🔔 Releases
-
-CoralGate has a built-in update checker, however it will not automatically install nor download the update, it will only send a message telling you if you are up to date or not.
-To prevent any malware from infecting your server, only download CoralGate from our **trusted sources**:
-- [Official GitHub](https://github.com/GTeamx/CoralGate/releases)
-
-Feel free to compile CoralGate yourself, however note that using a build from the dev branch will prevent you from getting support. If you want features or fixes to be added to CoralGate, please follow the [Issues/Feature request](https://github.com/GTeamx/CoralGate?tab=readme-ov-file#-issuesfeature-request) section.
-
-## 🚧 API
-
-CoralGate uses it's own API to determine if an IP is malicious or not.
+## API
-The API is hosted in Germarny and all data is processed within this country. This API doesn't require any key or subscription to be used. You can use it for free manually too.
+CoralGate utilizes a proprietary, custom-built API hosted in Germany (fully GDPR-compliant) to determine IP reputation. The API is free, requires no authentication keys, and supports both IPv4 and IPv6.
-To use the API manually, use the base URL: https://api.gteam.cloud/coralgate/v2/enter-ip-here
+You can manually query the API using the following structure:
+`https://api.gteam.cloud/coralgate/v2/`
-Simply replace "enter-ip-here" by the IP you wish to verify.
-E.g.: https://api.gteam.cloud/coralgate/v2/185.65.134.164 *(This is a known malicious ip address)*
-E.g.: https://api.gteam.cloud/coralgate/v2/9.9.9.9 *(This is not a malicious ip address)*
+**Examples:**
+- `https://api.gteam.cloud/coralgate/v2/185.65.134.164` *(Returns malicious)*
+- `https://api.gteam.cloud/coralgate/v2/9.9.9.9` *(Returns safe)*
-The API supports both IPv4 and IPv6 and has currently **2** major versions:
-- v2 *(latest)*
-- v1
+### False Positives
-## 🚷 API false positives
+If your ISP, domain name, or personal IP is falsely flagged and blocked from CoralGate-protected servers, please open a ticket on our [Discord](https://discord.gteam.cloud). Provide the affected IP addresses and their primary use case, and our support team will assist with whitelisting.
-If your API, ISP, domain name or holders get blocked by our API and therefore from the CoralGate powered servers, contact us on [Discord](https://discord.gteam.cloud) via our ticket system.
+**Traffic routinely blocked by the API:**
+Port/IP scanners, crawlers, MOTD/player-count fetchers, VPNs, proxies, TOR exit nodes, and automated hosting services (e.g., Shodan, OpenHeimer).
-Provide the impacted ip address(es) and further explanation on the usage behind the ip address(es). Our support team will try their best to help you and get you unblocked from CoralGate.
+**Traffic exempt from blocking:**
+Known voting sites and verified server lists.
-*Please note that we detect and block ip addresses that exerce the following behavior/service:*
-- Port scanning
-- IP scanning
-- Crawlers
-- MOTD fetchers
-- Online player count fetchers/monitors
-- VPN/Proxy
-- TOR
-- Hosting services
-- Cloud providers
-- Bots
-- Server finders/scanners (shodan, OpenHeimer...)
-....
+### Privacy & Disclosure
-*Some services are exempt from these rules, such as:*
-- Voting sites
-- Server lists
+By default, CoralGate automatically verifies the reputation of connecting players to block malicious traffic.
-## 🎯 Issues/Feature request
+* **Data Transmitted:** When a player attempts to join, their IP address is sent via a secure GET request to our proprietary API.
+* **Endpoint:** `https://api.gteam.cloud/coralgate/v2/`
+* **Privacy:** The API is hosted in Germany (fully GDPR-compliant). No personally identifiable information (PII) beyond the IP is processed, and data is used strictly for real-time risk assessment.
+* **Opt-Out:** You can entirely disable this external API lookup or route requests through your own custom endpoint by modifying the `config.yml` file.
-Before opening an issue or feature request make sure you follow our templates.
-If your issue/feature request goes unotice, you can open a ticket on our [Discord](https://discord.gteam.cloud) server or bump it.
+## Contributing & Support
-## 🔃 Contributing
+**Issues & Feature Requests:**
+Please utilize the provided GitHub templates when opening an issue or requesting a feature. If your ticket requires urgent attention, you may reference it in our Discord support channels.
-Before contributing, please make sure that you follow our conventions (naming scheme, indentation) and that your code works.
-Make sure to also precise on what platform and version your test was ran, with other plugins/mods installed (if applicable).
+**Contributing:**
+We welcome pull requests. Ensure your code follows our existing naming conventions and indentation standards. When submitting a PR, detail the platform, exact version, and any other relevant plugins used during your testing.
-## 📎 Special Credits
+## Credits & License
-Special credits to [retrooper](https://github.com/retrooper) for making [packetevents](https://github.com/retrooper/packetevents) that we are using to analyze packets!
-Thank you so much for your amazing work and dedication!
+CoralGate is built on top of these incredible open-source projects:
-## 📜 License
+* [packetevents](https://github.com/retrooper/packetevents) by [retrooper](https://github.com/retrooper) - Powers our core packet analysis.
+* [Lamp](https://github.com/Revxrsal/lamp) by [Revxrsal](https://github.com/Revxrsal) - Handles our cross-platform command ecosystem.
+* [bStats](https://github.com/Bastian/bStats) by [Bastian Oppermann](https://github.com/Bastian) - Provides anonymous usage metrics.
+* [Jankson](https://github.com/falkreon/Jankson) by [Falkreon](https://github.com/falkreon) - Parses our API JSON results.
+* [async-http-client](https://github.com/AsyncHttpClient/async-http-client) by [Aayush Atharva](https://github.com/hyperxpro) - Powers our network API queries and update checks.
+* [boosted-yaml](https://github.com/dejvokep/boosted-yaml) by [dejvokep](https://github.com/dejvokep) - Drives our internal configuration engine.
-This project is licensed under GNU General Public License v3.0 (GPL).
+This project is licensed under the [GNU General Public License v3.0 (GPL)](LICENSE).
diff --git a/build.gradle.kts b/build.gradle.kts
index c9f7d67..389ed16 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,15 +1,7 @@
-// Project related options.
-extra["coreVersion"] = "0.4.2-rc"
-
-// Dependency related options.
-extra["lampVersion"] = "4.0.0-rc.17"
-extra["packetEventsVersion"] = "2.13.0"
-extra["bstatsVersion"] = "3.2.1"
-
subprojects {
group = "cloud.gteam"
- version = "0.3.1-rc"
+ version = "0.4.0"
repositories {
mavenCentral()
diff --git a/bungeecord/build.gradle.kts b/bungeecord/build.gradle.kts
index ff1078c..85ebaeb 100644
--- a/bungeecord/build.gradle.kts
+++ b/bungeecord/build.gradle.kts
@@ -1,14 +1,14 @@
plugins {
`java-library`
- id("com.gradleup.shadow") version "9.4.2"
+ alias(libs.plugins.shadow)
}
java {
toolchain {
- languageVersion.set(JavaLanguageVersion.of(8))
+ languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -31,57 +31,50 @@ repositories {
dependencies {
- // Get versions.
- val lampVersion: String by rootProject.extra
- val packetEventsVersion: String by rootProject.extra
- val bstatsVersion: String by rootProject.extra
-
// Dependencies.
- implementation("org.bstats:bstats-bungeecord:$bstatsVersion")
- implementation("io.github.revxrsal:lamp.common:$lampVersion")
- implementation("io.github.revxrsal:lamp.bungee:$lampVersion")
+ implementation(libs.bstats.bungeecord)
+ implementation(libs.lamp.common)
+ implementation(libs.lamp.bungee)
- compileOnly("com.github.retrooper:packetevents-bungeecord:$packetEventsVersion")
- compileOnly("net.md-5:bungeecord-api:1.16-R0.4")
+ compileOnly(libs.packetevents.bungeecord)
+ compileOnly(libs.bungeecord.api)
// Core implementation.
- implementation(project(path = ":core", configuration = "shadow"))
+ compileOnly(project(":core"))
}
tasks.processResources {
- // Get versions.
- val packetEventsVersion: String by rootProject.extra
- val coreVersion: String by rootProject.extra
-
- // Replace bungee.yml
- filesMatching("bungee.yml") {
- expand("version" to project.version)
- }
+ inputs.property("name", project.name)
+ inputs.property("version", project.version)
+ inputs.property("coreVersion", libs.versions.coreVersion.get())
+ inputs.property("packeteventsVersion", libs.versions.packetevents.get())
// Replace properties.
- filesMatching("platform.properties") {
-
- expand(
- "name" to project.name,
- "version" to project.version,
- "coreVersion" to coreVersion,
- "packeteventsVersion" to packetEventsVersion
- )
-
+ filesMatching(listOf("bungee.yml", "platform.properties")) {
+ expand(inputs.properties)
}
+}
+
+// A trick so netty used by async-http-client is always
+// relocated, but netty used by injector is always provided by platform (spigot, bungeecord, velocity)
+val coreProvider = provider { project(":core").tasks.shadowJar.flatMap { it.archiveFile } }
+tasks.jar {
+ enabled = false // only shadowJar is used
}
tasks.shadowJar {
// Wait for the core shadowJar to finish.
- dependsOn(project(":core").tasks.named("shadowJar"))
+ dependsOn(":core:shadowJar")
+
+ archiveBaseName = "CoralGate-Bungeecord"
+ archiveVersion = project.version.toString()
+ archiveClassifier = ""
- archiveBaseName.set("CoralGate-Bungeecord")
- archiveVersion.set(project.version.toString())
- archiveClassifier.set("")
+ from(zipTree(coreProvider)) // include shadowed core
// Relocate bStats.
relocate("org.bstats", "cloud.gteam.coralgate.libs.bstats")
@@ -90,6 +83,10 @@ tasks.shadowJar {
exclude("META-INF/*.DSA")
exclude("META-INF/*.RSA")
+ filesMatching("META-INF/*.kotlin_module") {
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+ }
+
}
tasks.build {
diff --git a/bungeecord/src/main/java/cloud/gteam/coralgate/BungeePlugin.java b/bungeecord/src/main/java/cloud/gteam/coralgate/BungeePlugin.java
index 80df399..ee07961 100644
--- a/bungeecord/src/main/java/cloud/gteam/coralgate/BungeePlugin.java
+++ b/bungeecord/src/main/java/cloud/gteam/coralgate/BungeePlugin.java
@@ -21,6 +21,8 @@
import cloud.gteam.coralgate.commands.BungeePermissionChecker;
import cloud.gteam.coralgate.commands.CoralGateCommand;
import cloud.gteam.coralgate.commands.permissions.PermissionFactory;
+import cloud.gteam.coralgate.injector.BungeeInjector;
+import cloud.gteam.coralgate.injector.handlers.BungeeNettyResponder;
import cloud.gteam.coralgate.processor.NetworkProcessor;
import cloud.gteam.coralgate.utils.PlatformUtils;
import com.github.retrooper.packetevents.PacketEvents;
@@ -34,11 +36,15 @@
public final class BungeePlugin extends Plugin {
private final CorePlugin corePlugin = new CorePlugin();
+ private final BungeeInjector injector = new BungeeInjector();
@Override
public void onLoad() {
- PacketEvents.getAPI().getEventManager().registerListener(
- new NetworkProcessor(getCorePlugin()), PacketListenerPriority.HIGHEST);
+
+ this.injector.inject();
+
+ PacketEvents.getAPI().getEventManager().registerListener(new NetworkProcessor(getCorePlugin()), PacketListenerPriority.HIGHEST);
+
}
@Override
@@ -48,7 +54,7 @@ public void onEnable() {
new Metrics(this, 29439);
// Load core.
- this.corePlugin.onEnable(this.getLogger(), getDataFolder(), this.getProxy().getConfig().isOnlineMode(), "config.yml", PlatformUtils.loadProperties(this.getClass()));
+ this.corePlugin.onEnable(this.getLogger(), getDataFolder(), this.getProxy().getConfig().isOnlineMode(), "config.yml", PlatformUtils.loadProperties(this.getClass()), new BungeeNettyResponder());
// Load commands.
final Lamp bukkitCommandActor = BungeeLamp.builder(this)
diff --git a/bungeecord/src/main/java/cloud/gteam/coralgate/injector/BungeeInjector.java b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/BungeeInjector.java
new file mode 100644
index 0000000..12342f1
--- /dev/null
+++ b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/BungeeInjector.java
@@ -0,0 +1,118 @@
+/*
+ * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion
+ * Copyright (C) 2016-2022 ViaVersion 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 .
+ */
+
+package cloud.gteam.coralgate.injector;
+
+import cloud.gteam.coralgate.injector.connection.BungeeChannelInitializer;
+import com.github.retrooper.packetevents.util.reflection.Reflection;
+import io.github.retrooper.packetevents.injector.SetWrapper;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelInitializer;
+import net.md_5.bungee.api.ProxyServer;
+
+import java.lang.reflect.Field;
+import java.util.Set;
+
+public class BungeeInjector {
+
+ public static final String DECODER_NAME = "cg-decoder";
+ private static final Field LISTENERS_FIELD;
+
+ static {
+
+ LISTENERS_FIELD = Reflection.getField(ProxyServer.getInstance().getClass(), "listeners");
+ LISTENERS_FIELD.setAccessible(true);
+
+ }
+
+ public void injectChannel(final Channel channel) {
+
+ Field initializerField = null;
+ ChannelHandler bootstrapAcceptor = null;
+ for (String channelName : channel.pipeline().names()) {
+
+ if (channelName.contains("QueryHandler")) {
+ return; // query handler, abort injection.
+ }
+
+ final ChannelHandler handler = channel.pipeline().get(channelName);
+ if (handler == null) {
+ continue;
+ }
+
+ try {
+
+ final Field f = handler.getClass().getDeclaredField("childHandler");
+ f.setAccessible(true);
+ bootstrapAcceptor = handler;
+ initializerField = f;
+
+ } catch (final Exception ignored) {}
+
+ }
+
+ if (bootstrapAcceptor == null) {
+
+ bootstrapAcceptor = channel.pipeline().first();
+ try {
+
+ initializerField = bootstrapAcceptor.getClass().getDeclaredField("childHandler");
+ initializerField.setAccessible(true);
+
+ } catch (final NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ final ChannelInitializer newInitializer;
+ try {
+ newInitializer = new BungeeChannelInitializer(initializerField.get(bootstrapAcceptor));
+ } catch (final IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ initializerField.set(bootstrapAcceptor, newInitializer);
+ } catch (final IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ public void inject() {
+
+ try {
+
+ final Set listeners = (Set) LISTENERS_FIELD.get(ProxyServer.getInstance());
+
+ for (final Channel channel : listeners) {
+ injectChannel(channel);
+ }
+
+ final Set wrapper = new SetWrapper<>(listeners, this::injectChannel);
+ LISTENERS_FIELD.set(ProxyServer.getInstance(), wrapper);
+
+ } catch (final IllegalAccessException e) {
+ throw new RuntimeException("Failed to inject custom listeners into ProxyServer instance. See error: ", e);
+ }
+
+ }
+
+}
diff --git a/bungeecord/src/main/java/cloud/gteam/coralgate/injector/connection/BungeeChannelInitializer.java b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/connection/BungeeChannelInitializer.java
new file mode 100644
index 0000000..ad4b5f8
--- /dev/null
+++ b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/connection/BungeeChannelInitializer.java
@@ -0,0 +1,69 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import com.github.retrooper.packetevents.protocol.ConnectionState;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Method;
+
+public class BungeeChannelInitializer extends ChannelInitializer {
+
+ private static final Method INIT_CHANNEL_METHOD;
+
+ static {
+
+ try {
+
+ INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
+ INIT_CHANNEL_METHOD.setAccessible(true);
+
+ } catch (final NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ private final Object oldInitializer;
+
+ public BungeeChannelInitializer(final Object oldInitializer) {
+ this.oldInitializer = oldInitializer;
+ }
+
+ @Override
+ protected void initChannel(final @NotNull Channel channel) throws Exception {
+
+ if (!channel.isActive()) {
+ return;
+ }
+
+ INIT_CHANNEL_METHOD.invoke(this.oldInitializer, channel);
+
+ // No injection if "legacy-decoder" is not present.
+ if (channel.pipeline().get("legacy-decoder") == null) {
+ return;
+ }
+
+ ServerConnectionInitializer.initChannel(channel, ConnectionState.HANDSHAKING);
+
+ }
+
+}
diff --git a/bungeecord/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java
new file mode 100644
index 0000000..54cd06c
--- /dev/null
+++ b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java
@@ -0,0 +1,47 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import cloud.gteam.coralgate.injector.BungeeInjector;
+import cloud.gteam.coralgate.injector.handlers.BungeeDecoder;
+import cloud.gteam.coralgate.injector.handlers.BungeeLegacyKickRewriter;
+import com.github.retrooper.packetevents.protocol.ConnectionState;
+import com.github.retrooper.packetevents.protocol.player.User;
+import com.github.retrooper.packetevents.protocol.player.UserProfile;
+import io.netty.channel.Channel;
+
+public class ServerConnectionInitializer {
+
+ public static void addChannelHandlers(final Channel channel, final BungeeDecoder decoder) {
+ channel.pipeline().addBefore("legacy-decoder", BungeeInjector.DECODER_NAME, decoder);
+ channel.pipeline().addFirst("cg-legacy-kick-rewriter", new BungeeLegacyKickRewriter());
+ }
+
+ public static void initChannel(final Channel channel, final ConnectionState state) {
+
+ final BungeeDecoder decoder = new BungeeDecoder(new User(channel, state, null, new UserProfile(null, null)));
+ addChannelHandlers(channel, decoder);
+
+ }
+
+ public static void destroyChannel(final Channel channel) {
+ channel.pipeline().remove(BungeeInjector.DECODER_NAME);
+ }
+
+}
diff --git a/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeDecoder.java b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeDecoder.java
new file mode 100644
index 0000000..cb49bda
--- /dev/null
+++ b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeDecoder.java
@@ -0,0 +1,143 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.handlers;
+
+import cloud.gteam.coralgate.injector.connection.ServerConnectionInitializer;
+import com.github.retrooper.packetevents.PacketEvents;
+import com.github.retrooper.packetevents.event.simple.PacketHandshakeReceiveEvent;
+import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
+import com.github.retrooper.packetevents.protocol.player.User;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageDecoder;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class BungeeDecoder extends MessageToMessageDecoder {
+
+ public final User user;
+
+ public BungeeDecoder(final User user) {
+ this.user = user;
+ }
+
+ public void read(final ChannelHandlerContext ctx, final ByteBuf byteBuf, final List output) throws Exception {
+
+ final int firstReaderIndex = byteBuf.readerIndex();
+ final int readable = byteBuf.readableBytes();
+
+ boolean isLegacyServerListPing = false;
+ boolean isBareSingleByte = false;
+
+ // Detect every legacy server list ping variant by peeking bytes directly (never consuming
+ // the reader index), rather than parsing a VarInt. A VarInt read would throw on the
+ // single-byte pre-1.4 variant since there's no second byte to complete it.
+ if (readable >= 1 && byteBuf.getUnsignedByte(firstReaderIndex) == 0xFE) {
+
+ // Bare single 0xFE, nothing trailing, pre-1.4 clients (1.1, 1.2, 1.3).
+ if (readable == 1) {
+
+ isLegacyServerListPing = true;
+
+ // Bare single 0xFE, pre-1.4 clients (1.1, 1.2, 1.3).
+ // NOTE: packetevents' PacketHandshakeReceiveEvent/ProtocolPacketEvent cannot be
+ // constructed for a 1-byte buffer (it throws trying to read a packet id), so this
+ // variant is handled directly here and never routed through the event system.
+ isBareSingleByte = true;
+
+ } else if (byteBuf.getUnsignedByte(firstReaderIndex + 1) == 0x01) {
+
+ if (readable == 2) {
+
+ // FE 01, nothing trailing, 1.4/1.5 clients.
+ isLegacyServerListPing = true;
+
+ } else if (readable >= 3 && byteBuf.getUnsignedByte(firstReaderIndex + 2) == 0xFA) {
+
+ // FE 01 FA ..., 1.6 clients.
+ isLegacyServerListPing = true;
+
+ }
+
+ }
+
+ }
+
+ if (!isLegacyServerListPing) {
+
+ output.add(byteBuf.retain());
+ return;
+
+ }
+
+ if (isBareSingleByte) {
+
+ // Can't fire a PacketHandshakeReceiveEvent for this, just drop it silently.
+ // If you need NetworkProcessor to see/respond to this variant too, that logic
+ // needs to move here instead (can't go through packetevents' event system).
+ ByteBufHelper.clear(byteBuf);
+ return;
+
+ }
+
+ final PacketHandshakeReceiveEvent packetReceiveEvent = new PacketHandshakeReceiveEvent(ctx.channel(), this.user, null, byteBuf, false);
+ PacketEvents.getAPI().getEventManager().callEvent(packetReceiveEvent, () -> byteBuf.readerIndex(byteBuf.readerIndex()));
+
+ // No action is taken about the legacy packet here.
+ // NetworkProcessor receives the packet (like any other packet), and then decides what to do,
+ // including which legacy reply format to use (it re-derives that from the buffer itself).
+ // If the packet is canceled, it's cleared. Else we just pass it as any normal packet would.
+ if (packetReceiveEvent.isCancelled()) {
+ ByteBufHelper.clear(byteBuf);
+ } else {
+
+ byteBuf.readerIndex(firstReaderIndex);
+ output.add(byteBuf.retain());
+
+ }
+
+ if (packetReceiveEvent.hasPostTasks()) {
+
+ for (final Runnable task : packetReceiveEvent.getPostTasks()) {
+ task.run();
+ }
+
+ }
+
+ }
+
+ @Override
+ public void decode(final ChannelHandlerContext ctx, final ByteBuf buffer, final List out) throws Exception {
+
+ if (buffer.isReadable()) {
+ read(ctx, buffer, out);
+ }
+
+ }
+
+ @Override
+ public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
+
+ ServerConnectionInitializer.destroyChannel(ctx.channel());
+ super.channelInactive(ctx);
+
+ }
+
+}
diff --git a/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeLegacyKickRewriter.java b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeLegacyKickRewriter.java
new file mode 100644
index 0000000..c8ba101
--- /dev/null
+++ b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeLegacyKickRewriter.java
@@ -0,0 +1,60 @@
+package cloud.gteam.coralgate.injector.handlers;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+
+import java.nio.charset.StandardCharsets;
+
+public class BungeeLegacyKickRewriter extends ChannelOutboundHandlerAdapter {
+
+ public BungeeLegacyKickRewriter() {}
+
+ @Override
+ public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception {
+
+ if (!(msg instanceof ByteBuf)) {
+ super.write(ctx, msg, promise);
+ return;
+ }
+
+ final ByteBuf buf = (ByteBuf) msg;
+
+ final int readable = buf.readableBytes();
+
+ // Legacy kick shape: 0xFF + short length (UTF-16 char count) + UTF-16BE string.
+ if (readable < 3 || (buf.getUnsignedByte(buf.readerIndex()) != 0xFF)) {
+
+ super.write(ctx, msg, promise);
+ return;
+
+ }
+
+ final int readerIndex = buf.readerIndex();
+ final byte[] raw = new byte[readable];
+ buf.getBytes(readerIndex, raw);
+
+ final String decoded = new String(raw, 3, readable - 3, StandardCharsets.UTF_16BE);
+ if (!decoded.startsWith("§cOutdated client! Please use ")) {
+
+ super.write(ctx, msg, promise);
+ return;
+
+ }
+
+ final String rewritten = "§cOutdated client! Please use " + "1.21.11"; // TODO: make it read from config.
+ final byte[] rewrittenBytes = rewritten.getBytes(StandardCharsets.UTF_16BE);
+
+ final ByteBuf newBuf = ctx.alloc().buffer();
+ newBuf.writeByte(0xFF);
+ newBuf.writeShort(rewritten.length());
+ newBuf.writeBytes(rewrittenBytes);
+
+ // We're replacing this buffer entirely, release the original.
+ buf.release();
+ super.write(ctx, newBuf, promise);
+
+ }
+
+}
diff --git a/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeNettyResponder.java b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeNettyResponder.java
new file mode 100644
index 0000000..91aed4d
--- /dev/null
+++ b/bungeecord/src/main/java/cloud/gteam/coralgate/injector/handlers/BungeeNettyResponder.java
@@ -0,0 +1,70 @@
+/*
+ * This file is part of CoralGate - https://github.com/GTeamX/CoralGate
+ * Copyright (C) 2026 GTeamX (GTeam) and it's 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 .
+ */
+
+package cloud.gteam.coralgate.injector.handlers;
+
+import cloud.gteam.coralgate.injector.NettyResponder;
+import com.github.retrooper.packetevents.protocol.player.User;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+
+import java.nio.charset.StandardCharsets;
+
+public class BungeeNettyResponder implements NettyResponder {
+
+ public BungeeNettyResponder() {}
+
+ @Override
+ public void sendLegacyPingResponse(final User user, final int protocolVersion, final String serverVersion, final String motd, final int onlinePlayers, final int maxPlayers) {
+
+ final String response = "§1\0"
+ + protocolVersion + "\0"
+ + serverVersion + "\0"
+ + motd + "\0"
+ + onlinePlayers + "\0"
+ + maxPlayers;
+ write(user, response);
+
+ }
+
+ @Override
+ public void sendOldLegacyPingResponse(final User user, final String motd, final int onlinePlayers, final int maxPlayers) {
+
+ final String response = motd + "§" + onlinePlayers + "§" + maxPlayers;
+ write(user, response);
+
+ }
+
+ private void write(final User user, final String response) {
+
+ final Channel channel = (Channel) user.getChannel();
+ final byte[] responseBytes = response.getBytes(StandardCharsets.UTF_16BE);
+
+ final ByteBuf buf = channel.alloc().buffer();
+ buf.writeByte(0xFF);
+ buf.writeShort(response.length());
+ buf.writeBytes(responseBytes);
+
+ channel.pipeline().firstContext()
+ .writeAndFlush(buf)
+ .addListener(ChannelFutureListener.CLOSE);
+
+ }
+
+}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index b8cf10b..d109d73 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,19 +1,17 @@
-import org.gradle.kotlin.dsl.shadowJar
-
plugins {
`java-library`
- id("com.gradleup.shadow") version "9.4.2"
+ alias(libs.plugins.shadow)
}
group = "cloud.gteam.coralgate"
-version = rootProject.extra["coreVersion"]!!
+version = libs.versions.coreVersion.get()
java {
toolchain {
- languageVersion.set(JavaLanguageVersion.of(8))
+ languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -32,39 +30,44 @@ repositories {
dependencies {
- // Get versions.
- val lampVersion: String by rootProject.extra
- val packetEventsVersion: String by rootProject.extra
-
// 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")
+ implementation(libs.lamp.common)
+ implementation(libs.jankson)
+ implementation(libs.async.http.client)
+ implementation(libs.jetbrains.annotations)
+ implementation(libs.boosted.yaml)
- compileOnly("com.github.retrooper:packetevents-api:$packetEventsVersion")
+ compileOnly(libs.packetevents.api)
+ compileOnly(libs.gson)
+ compileOnly(libs.adventure.api)
}
tasks.processResources {
- val props = mapOf("version" to version)
- inputs.properties(props)
+ inputs.property("version", version)
filteringCharset = "UTF-8"
filesNotMatching("**/*.png") {
- expand(props)
+ expand(inputs.properties)
}
}
+tasks.jar {
+ enabled = false // only shadowJar is used
+}
+
tasks.shadowJar {
- archiveClassifier.set("") // produce core.jar instead of core-all.jar
+ archiveClassifier = "" // produce core.jar instead of core-all.jar
relocate("dev.dejvokep.boostedyaml", "cloud.gteam.coralgate.libs.boostedyaml")
+ relocate("io.netty", "cloud.gteam.coralgate.libs.netty")
+
+ filesMatching("META-INF/*.kotlin_module") {
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+ }
}
diff --git a/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java b/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java
index 58c0716..b697d2b 100644
--- a/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java
+++ b/core/src/main/java/cloud/gteam/coralgate/CorePlugin.java
@@ -20,6 +20,7 @@
import cloud.gteam.coralgate.api.APIManager;
import cloud.gteam.coralgate.config.ConfigManager;
+import cloud.gteam.coralgate.injector.NettyResponder;
import cloud.gteam.coralgate.update.UpdateChecker;
import cloud.gteam.coralgate.utils.ConfigUtils;
import com.github.retrooper.packetevents.PacketEvents;
@@ -27,6 +28,7 @@
import java.io.File;
import java.util.Objects;
import java.util.Properties;
+import java.util.concurrent.Executors;
import java.util.logging.Logger;
public final class CorePlugin {
@@ -40,11 +42,13 @@ public final class CorePlugin {
private Properties platformProperties;
+ private NettyResponder nettyResponder;
+
private ConfigManager configManager;
private APIManager apiManager;
private UpdateChecker updateChecker;
- public void onEnable(final Logger logger, final File dataFolder, final boolean onlineMode, final String configFileName, final Properties platformProperties) {
+ public void onEnable(final Logger logger, final File dataFolder, final boolean onlineMode, final String configFileName, final Properties platformProperties, final NettyResponder nettyResponder) {
CorePlugin.logger = logger;
@@ -54,13 +58,17 @@ public void onEnable(final Logger logger, final File dataFolder, final boolean o
logger.info("Loading platform '" + platformProperties.getProperty("platform-name") + "' version '" + platformProperties.getProperty("platform-version") + "', implemented against core version '" + platformProperties.getProperty("core-version") + "'...");
+ this.nettyResponder = nettyResponder;
+
final String peCoreVersion = platformProperties.getProperty("packetevents-version");
final String peServerVersion = PacketEvents.getAPI().getVersion().toString();
// 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 +79,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 +103,15 @@ 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).");
+ }
+
+ logger.info("Reading online-mode and compression threshold...");
this.onlineMode = onlineMode;
@@ -105,19 +121,7 @@ public void onEnable(final Logger logger, final File dataFolder, final boolean o
this.updateChecker = new UpdateChecker(this);
- this.updateChecker.isUpToDate().thenAccept(upToDate -> {
-
- try {
- Thread.sleep(3000);
- } catch (final InterruptedException ignored) {}
-
- if (upToDate) {
- CorePlugin.getLogger().info("CoralGate is up to date!");
- } else {
- CorePlugin.getLogger().warning("You are behind updates on CoralGate! Latest version is '" + this.updateChecker.getLatestVersion() + "'. You are on '" + this.platformProperties.getProperty("platform-version") + "'.");
- }
-
- });
+ Executors.newSingleThreadScheduledExecutor().schedule(() -> this.updateChecker.checkForUpdates(), 3, java.util.concurrent.TimeUnit.SECONDS);
logger.info("CoralGate is ready to use!");
@@ -125,8 +129,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();
+ }
}
@@ -150,6 +159,10 @@ public Properties getPlatformProperties() {
return this.platformProperties;
}
+ public NettyResponder getNettyResponder() {
+ return this.nettyResponder;
+ }
+
public ConfigManager getConfigManager() {
return this.configManager;
}
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..cfc7d8c 100644
--- a/core/src/main/java/cloud/gteam/coralgate/api/APIManager.java
+++ b/core/src/main/java/cloud/gteam/coralgate/api/APIManager.java
@@ -73,36 +73,57 @@ public CompletableFuture isIpBlocked(final String ipAddress) {
// Cache not available, fetch from API.
return fetchFromApi(ipAddress).thenApply(result -> {
- if (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) this.ipCache.put(ipAddress, new CacheEntry(result));
+
+ if (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) {
+ this.ipCache.put(ipAddress, new CacheEntry(result));
+ }
+
return result;
+
});
}
- public boolean isIpBlockedCache(final String ipAddress) {
+ public boolean isIpCached(final String ipAddress) {
final CacheEntry entry = this.ipCache.get(ipAddress);
- return entry != null && !entry.isExpired(this.cacheTime) && !entry.isBlocked();
+ return entry != null && !entry.isExpired(this.cacheTime);
+ }
+
+ public boolean isIpCachedBlocked(final String ipAddress) {
+ return isIpCached(ipAddress) && this.ipCache.get(ipAddress).isBlocked();
}
- public void reportIp(final String ipAddress) {
- fetchFromApi(ipAddress);
+ public void checkIp(final String ipAddress) {
+ fetchFromApi(ipAddress).thenApply(result -> {
+
+ if (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) {
+ this.ipCache.put(ipAddress, new CacheEntry(result));
+ }
+
+ return null;
+
+ });
}
private CompletableFuture fetchFromApi(final String ipAddress) {
- if (!this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) return CompletableFuture.completedFuture(false);
+ if (!this.corePlugin.getConfigManager().getConfig().isAllowApiUsage()) {
+ return CompletableFuture.completedFuture(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 CompletableFuture.completedFuture(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;
+ // Not blocked.
return CompletableFuture.completedFuture(false);
}
@@ -115,12 +136,12 @@ 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;
@@ -139,9 +160,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 +177,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 +208,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 +237,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());
}
diff --git a/core/src/main/java/cloud/gteam/coralgate/api/CacheEntry.java b/core/src/main/java/cloud/gteam/coralgate/api/CacheEntry.java
index 5d84ef7..f2e3882 100644
--- a/core/src/main/java/cloud/gteam/coralgate/api/CacheEntry.java
+++ b/core/src/main/java/cloud/gteam/coralgate/api/CacheEntry.java
@@ -33,7 +33,7 @@ public boolean isBlocked() {
}
public boolean isExpired(final long duration) {
- return (System.currentTimeMillis() - timestamp) > duration;
+ return (System.currentTimeMillis() - this.timestamp) > duration;
}
}
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..0b4e7df 100644
--- a/core/src/main/java/cloud/gteam/coralgate/commands/CoralGateCommand.java
+++ b/core/src/main/java/cloud/gteam/coralgate/commands/CoralGateCommand.java
@@ -28,6 +28,8 @@
import revxrsal.commands.annotation.Subcommand;
import revxrsal.commands.command.CommandActor;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
@Command({"coralgate", "cg"})
@@ -48,21 +50,37 @@ public void help(final CommandActor actor) {
final ConfigManager configManager = this.corePlugin.getConfigManager();
final ConfigModel config = configManager.getConfig();
- actor.sendRawMessage("§7-----------------------------------------------------");
- actor.sendRawMessage("");
- actor.sendRawMessage(config.getNormalPrefix() + "§f" + this.corePlugin.getPlatformProperties().getProperty("platform-version") + " | §7§o(" + this.corePlugin.getPlatformProperties().getProperty("core-version") + ")§r");
- actor.sendRawMessage("§7Made by XIII___ and Vagdedes2 with(out) love!");
- actor.sendRawMessage("");
- actor.sendRawMessage("§7Available commands:");
- actor.sendRawMessage("");
- actor.sendRawMessage(" - /coralgate§8|cg§r help §7- Show this menu.");
- actor.sendRawMessage(" - /coralgate§8|cg§r version§8|ver§r §7- Show versions.");
- actor.sendRawMessage(" - /coralgate§8|cg§r config§8|cfg§r reload§8|rl§r §7- Reload the config.");
- actor.sendRawMessage("");
- actor.sendRawMessage("§bHaving troubles ? Need help ? Found a bug ?");
- actor.sendRawMessage("§7Join our Discord: §bhttps://discord.gteam.cloud");
- actor.sendRawMessage("");
- actor.sendRawMessage("§7-----------------------------------------------------");
+ final String[] lines = {
+ "§7-----------------------------------------------------",
+ "",
+ config.getNormalPrefix() + "§f" + this.corePlugin.getPlatformProperties().getProperty("platform-version") + " | §7§o(" + this.corePlugin.getPlatformProperties().getProperty("core-version") + ")§r",
+ "§7Made by XIII___ and Vagdedes2 with(out) love!",
+ "",
+ "§7Available commands:",
+ "",
+ " - /coralgate§8|cg§r help §7- Show this menu.",
+ " - /coralgate§8|cg§r version§8|ver§r §7- Show versions.",
+ "",
+ "§bHaving troubles ? Need help ? Found a bug ?",
+ "§7Join our Discord: §bhttps://discord.gteam.cloud",
+ "",
+ "§7-----------------------------------------------------"
+ };
+
+ for (String line : lines) {
+
+ // Velocity only sees colors via "&" and not "§".
+ // However, other platforms only see colors via "§", so we'll have to handle this specifically for Velocity.
+ if (this.corePlugin.getPlatformProperties().getProperty("platform-name").equals("velocity")) {
+ line = line.replace("§", "&");
+ } else {
+ // Fool-proof in case somebody put "&" in their config for prefixes.
+ line = line.replace("&", "§");
+ }
+
+ actor.reply(line);
+
+ }
}
@@ -77,52 +95,51 @@ public void version(final CommandActor actor) {
final String currentVersion = this.corePlugin.getPlatformProperties().getProperty("core-version") + "_" + this.corePlugin.getPlatformProperties().getProperty("platform-version");
- actor.sendRawMessage("§7-----------------------------------------------------");
- actor.sendRawMessage("");
- actor.sendRawMessage(config.getNormalPrefix() + "§fVersions information");
- actor.sendRawMessage("");
- actor.sendRawMessage("§7Platform: §f" + this.corePlugin.getPlatformProperties().getProperty("platform-name"));
- actor.sendRawMessage("§7Version: " + (Objects.equals(this.corePlugin.getUpdateChecker().getLatestVersion(), currentVersion) ? "§a" : "§e") + this.corePlugin.getPlatformProperties().getProperty("platform-version"));
- actor.sendRawMessage("");
- actor.sendRawMessage("§7Core version: " + (Objects.equals(this.corePlugin.getUpdateChecker().getLatestVersion(), currentVersion) ? "§a" : "§e") + this.corePlugin.getPlatformProperties().getProperty("core-version"));
- actor.sendRawMessage("");
- actor.sendRawMessage("§7Configuration version: §f" + (Objects.equals(this.corePlugin.getConfigManager().getLatestConfigVersion(), config.getConfigVersion()) ? "§a" : "§e") + config.getConfigVersion());
- actor.sendRawMessage("");
+ final String versionColor = Objects.equals(this.corePlugin.getUpdateChecker().getLatestVersion(), currentVersion) ? "§a" : "§e";
+ final String configColor = Objects.equals(this.corePlugin.getConfigManager().getLatestConfigVersion(), config.getConfigVersion()) ? "§a" : "§e";
+ final String packeteventsColor = PacketEvents.getAPI().getVersion().toString().equals(this.corePlugin.getPlatformProperties().getProperty("packetevents-version")) ? "§a" : "§e";
+
+ // Create an array list so we can freely handle conditional statements with {} blocks
+ final List lines = new ArrayList<>();
+
+ lines.add("§7-----------------------------------------------------");
+ lines.add("");
+ lines.add(config.getNormalPrefix() + "§fVersions information");
+ lines.add("");
+ lines.add("§7Platform: §f" + this.corePlugin.getPlatformProperties().getProperty("platform-name"));
+ lines.add("§7Version: " + versionColor + this.corePlugin.getPlatformProperties().getProperty("platform-version"));
+ lines.add("");
+ lines.add("§7Core version: " + versionColor + this.corePlugin.getPlatformProperties().getProperty("core-version"));
+ lines.add("");
+ lines.add("§7Configuration version: §f" + configColor + config.getConfigVersion());
+ lines.add("");
+
if (config.isAllowApiUsage()) {
- actor.sendRawMessage("§7API host: §f" + config.getApiHost());
- actor.sendRawMessage("§7API version: §f" + config.getApiVersion());
+ lines.add("§7API host: §f" + config.getApiHost());
+ lines.add("§7API version: §f" + config.getApiVersion());
} else {
- actor.sendRawMessage("§7API usage is disabled.");
+ lines.add("§7API usage is disabled.");
}
- actor.sendRawMessage("");
- actor.sendRawMessage("§7packetevents version: " + (PacketEvents.getAPI().getVersion().toString().equals(this.corePlugin.getPlatformProperties().getProperty("packetevents-version")) ? "§a" : "§e") + this.corePlugin.getPlatformProperties().getProperty("packetevents-version"));
- actor.sendRawMessage("");
- actor.sendRawMessage("§7-----------------------------------------------------");
- }
+ lines.add("");
+ lines.add("§7packetevents version: " + packeteventsColor + this.corePlugin.getPlatformProperties().getProperty("packetevents-version"));
+ lines.add("");
+ lines.add("§7-----------------------------------------------------");
- // /coralgate|cg config|cfg reload|rl.
- @Subcommand({"config reload", "config rl", "cfg reload", "cfg rl"})
- @Description("Reload the configuration file of CoralGate.")
- @CommandPermission("coralgate.commands.config.reload")
- public void configReload(final CommandActor actor) {
+ for (String line : lines) {
- final ConfigManager configManager = this.corePlugin.getConfigManager();
- ConfigModel config = configManager.getConfig();
+ // Velocity only sees colors via "&" and not "§".
+ // However, other platforms only see colors via "§", so we'll have to handle this specifically for Velocity.
+ if (this.corePlugin.getPlatformProperties().getProperty("platform-name").equals("velocity")) {
+ line = line.replace("§", "&");
+ } else {
+ // Fool-proof in case somebody put "&" in their config for prefixes.
+ line = line.replace("&", "§");
+ }
- actor.sendRawMessage(config.getNormalPrefix() + "Reloading configuration file...");
+ actor.reply(line);
- // Get latest config version.
- final String latestConfigVersion = this.corePlugin.getConfigManager().getLatestConfigVersion();
-
- configManager.load();
- // Update config with latest load.
- 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() + "'.");
-
- 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 1103921..acc5782 100644
--- a/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java
+++ b/core/src/main/java/cloud/gteam/coralgate/config/ConfigManager.java
@@ -31,7 +31,7 @@
public class ConfigManager {
- private final String latestConfigVersion = "0.2.1";
+ private final String latestConfigVersion = "0.2.3";
private final String configFileName;
private final File configFile;
@@ -51,8 +51,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();
@@ -81,7 +83,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"));
@@ -107,7 +109,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/paper/src/main/java/cloud/gteam/coralgate/commands/PaperPermissionChecker.java b/core/src/main/java/cloud/gteam/coralgate/injector/NettyResponder.java
similarity index 57%
rename from paper/src/main/java/cloud/gteam/coralgate/commands/PaperPermissionChecker.java
rename to core/src/main/java/cloud/gteam/coralgate/injector/NettyResponder.java
index 2044f67..56e5ca9 100644
--- a/paper/src/main/java/cloud/gteam/coralgate/commands/PaperPermissionChecker.java
+++ b/core/src/main/java/cloud/gteam/coralgate/injector/NettyResponder.java
@@ -16,17 +16,16 @@
* along with this program. If not, see .
*/
-package cloud.gteam.coralgate.commands;
+package cloud.gteam.coralgate.injector;
-import cloud.gteam.coralgate.commands.permissions.PermissionChecker;
-import revxrsal.commands.bukkit.actor.BukkitCommandActor;
-import revxrsal.commands.command.CommandActor;
+import com.github.retrooper.packetevents.protocol.player.User;
-public class PaperPermissionChecker implements PermissionChecker {
+public interface NettyResponder {
- @Override
- public boolean hasPermission(final CommandActor actor, final String permission) {
- return ((BukkitCommandActor) actor).sender().hasPermission(permission);
- }
+ // 1.4+ format: "§1\0protocol\0version\0motd\0online\0max".
+ void sendLegacyPingResponse(final User user, final int protocolVersion, final String serverVersion, final String motd, final int onlinePlayers, final int maxPlayers);
+
+ // Pre-1.4 (<=1.3) format: "motd§online§max", no §1 prefix, no protocol/version fields.
+ void sendOldLegacyPingResponse(final User user, final String motd, final int onlinePlayers, final int maxPlayers);
}
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..56477a5 100644
--- a/core/src/main/java/cloud/gteam/coralgate/processor/NetworkProcessor.java
+++ b/core/src/main/java/cloud/gteam/coralgate/processor/NetworkProcessor.java
@@ -19,24 +19,40 @@
package cloud.gteam.coralgate.processor;
import cloud.gteam.coralgate.CorePlugin;
+import cloud.gteam.coralgate.injector.NettyResponder;
import com.github.retrooper.packetevents.PacketEvents;
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.netty.buffer.ByteBufHelper;
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.server.WrapperConfigServerDisconnect;
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.login.server.WrapperLoginServerLoginSuccess;
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDisconnect;
import com.github.retrooper.packetevents.wrapper.status.client.WrapperStatusClientPing;
+import com.github.retrooper.packetevents.wrapper.status.server.WrapperStatusServerPong;
import com.github.retrooper.packetevents.wrapper.status.server.WrapperStatusServerResponse;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.Style;
+import net.kyori.adventure.text.format.TextDecoration;
-import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public class NetworkProcessor implements PacketListener {
@@ -49,228 +65,329 @@ public NetworkProcessor(final CorePlugin corePlugin) {
this.corePlugin = corePlugin;
}
- // Incoming packets (Client -> Server) [C->S]
+ // Incoming packets (Client -> Server) [C->S].
public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) {
final InetSocketAddress inetSocketAddress = packetReceiveEvent.getSocketAddress();
- final String ipAddress = inetSocketAddress.getHostString();
// Exempt local IP addresses according to configuration file.
- if (this.corePlugin.getConfigManager().getConfig().isIgnoreLocalAddresses() && !this.corePlugin.isTestMode()) {
+ if (exemptLocalIpAddress(inetSocketAddress)) {
+ return;
+ }
- try {
+ final String ipAddress = inetSocketAddress.getHostString();
+ final User packetUser = packetReceiveEvent.getUser();
+ final PacketTypeCommon packetTypeCommon = packetReceiveEvent.getPacketType();
- final InetAddress inetAddress = InetAddress.getByName(ipAddress);
+ // Match MOTD related packets.
+ final boolean isMOTDPacket = packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE
+ || packetTypeCommon == PacketType.Status.Client.REQUEST
+ || packetTypeCommon == PacketType.Status.Client.PING;
- if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) return;
+ // Match login sequence related packets.
+ final boolean isLoginSequencePacket = packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE
+ || packetTypeCommon == PacketType.Login.Client.LOGIN_START
+ || packetTypeCommon == PacketType.Login.Client.ENCRYPTION_RESPONSE
+ || packetTypeCommon == PacketType.Login.Client.LOGIN_SUCCESS_ACK;
- } catch (final IOException ignored) {}
+ boolean isInvalidProtocol = false;
- }
+ // Check client's protocol & client version.
+ // Only match MOTD/login sequence related packets to save resources.
+ if (isMOTDPacket || isLoginSequencePacket) {
- final PacketTypeCommon packetTypeCommon = packetReceiveEvent.getPacketType();
+ // If the client's version/protocol version is invalid.
+ //noinspection ConstantValue
+ if (packetReceiveEvent.getUser().getClientVersion() == null
+ || packetReceiveEvent.getUser().getClientVersion().getProtocolVersion() < ClientVersion.getOldest().getProtocolVersion()
+ || packetReceiveEvent.getUser().getClientVersion().getProtocolVersion() > ClientVersion.getLatest().getProtocolVersion()) {
- /*
- * SOURCE PORT FILTERING.
- */
+ // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid protocol version.", !isMOTDPacket);
- /* Check different condition to trigger a MOTD packet check and blockage. */
+ // To send fake MOTD later down the line.
+ isInvalidProtocol = true;
- // This is the lowest dynamic port used by Linux.
- // Anything bellow means the port was forced to use that port and is therefore, not a real Minecraft client.
- final boolean invalidPort = inetSocketAddress.getPort() < 32768;
+ // Block further logic.
+ if (!isMOTDPacket) return;
- // Match MOTD related packets.
- final boolean isStatusPacket = packetTypeCommon == PacketType.Status.Client.PING
- || packetTypeCommon == PacketType.Status.Client.REQUEST
- || packetTypeCommon == PacketType.Handshaking.Client.LEGACY_SERVER_LIST_PING;
+ }
+
+ }
- /* Filter only packets that are used to get information about the server. */
+ // This is the lowest dynamic port used by Windows & Mac.
+ // Since Linux players are "rare", we'll issue a warning statement about them.
+ // Alongside that, we will block any server list ping to prevent bots from getting information about the server.
+ // (server version, online players, player count...).
+ final boolean isSuspiciousPort = inetSocketAddress.getPort() < 49152;
- // Handshake is also a MOTD related packet in a certain state.
- if (isStatusPacket || packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE) {
+ // This is the lowest dynamic port used by Linux.
+ // Anything bellow means the port was forcefully used and is therefore not a real Minecraft client.
+ final boolean isInvalidPort = inetSocketAddress.getPort() < 32768;
- // This is the lowest dynamic port used by Windows & Mac.
- // Since Linux players are "rare", we'll issue a warning statement about them.
- // Alongside that, we will block any server list ping to prevent bots from getting information about the server.
- // (server version, online players, player count...).
- final boolean suspiciousPort = inetSocketAddress.getPort() < 49152;
+ // Englobe suspicious port (which also includes invalid port) and bad protocol version.
+ final boolean isBadPacket = isSuspiciousPort || isInvalidProtocol;
- // Block the first incoming MOTD related packet of an IP.
- final boolean processMOTD = suspiciousPort
- || !this.corePlugin.getApiManager().isIpBlockedCache(ipAddress)
- || !this.corePlugin.getApiManager().isHealthy();
+ // Connection initialization for both MOTD and login procedures. (cross versions, cross-platform).
+ if (packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE) {
- if (suspiciousPort) {
+ this.connectionState.put(inetSocketAddress, packetTypeCommon);
- // 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() + "]");
+ // Forcefully check the IP so it can receive the real MOTD next time (if it's legit/safe).
+ // This also allows us to "pre cache" when the client will actually join the server.
+ this.corePlugin.getApiManager().checkIp(ipAddress);
- // Report the IP to CoralGate API.
- this.corePlugin.getApiManager().reportIp(ipAddress);
+ final WrapperHandshakingClientHandshake wrapperHandshakingClientHandshake = new WrapperHandshakingClientHandshake(packetReceiveEvent);
- }
+ // Drop the packet if it's suspicious and if it's a STATUS packet.
+ if (wrapperHandshakingClientHandshake.getIntention() == WrapperHandshakingClientHandshake.ConnectionIntention.STATUS
+ && wrapperHandshakingClientHandshake.getNextConnectionState() == ConnectionState.STATUS) {
- if (processMOTD) {
+ if (isBadPacket) {
- // Packet responsible for the latency showup.
- if (packetTypeCommon == PacketType.Status.Client.PING || packetTypeCommon == PacketType.Handshaking.Client.LEGACY_SERVER_LIST_PING) {
+ // Decide reason based on port.
+ String reason = isInvalidPort
+ ? "Invalid port used by client."
+ : "Suspicious port used by client.";
- // Cancel packet and send a "forged" response.
- packetReceiveEvent.setCancelled(true);
- packetReceiveEvent.getUser().sendPacketSilently(new WrapperStatusClientPing(packetReceiveEvent));
+ // Decide reason based on invalid protocol (or port).
+ reason = isInvalidProtocol
+ ? "Invalid protocol version."
+ : reason;
- // Block further logic.
- return;
+ // Don't cancel the packet else the REQUEST/PING fail.
+ CorePlugin.getLogger().warning(reason + " Keep an eye on " + inetSocketAddress + ". [C->S | " + packetReceiveEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]");
}
- // Packet responsible for the MOTD message and server related information (player count, version).
- if (packetTypeCommon == PacketType.Status.Client.REQUEST) {
+ // Drop the packet if it's suspicious, close connection if it's invalid.
+ // Only if it's a LOGIN packet.
+ } else if (wrapperHandshakingClientHandshake.getIntention() == WrapperHandshakingClientHandshake.ConnectionIntention.LOGIN
+ && wrapperHandshakingClientHandshake.getNextConnectionState() == ConnectionState.LOGIN) {
- // Cancel the packet and send a forged generic looking MOTD.
- packetReceiveEvent.setCancelled(true);
- packetReceiveEvent.getUser().sendPacketSilently(new WrapperStatusServerResponse(getForgedMOTD()));
+ // Close the connection.
+ if (isInvalidPort || isInvalidProtocol) {
- // Block further logic.
- return;
+ // Decide reason based off if the port is invalid or if the protocol version is.
+ final String reason = isInvalidPort
+ ? "Invalid port used by client."
+ : "Invalid protocol version.";
+
+ // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason, true);
+
+ // Log an alert but don't close the connection.
+ } else if (isSuspiciousPort) {
+
+ // Don't cancel the packet else the LOGIN_START procedure fails.
+ CorePlugin.getLogger().warning("Suspicious port used by client. Keep an eye on " + inetSocketAddress + ". [C->S | " + packetReceiveEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]");
}
- // Specific 'STATUS' handshake state.
- // Yes the condition is "always true", but I prefer to keep this in case the protocol changes in future releases.
- if (packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE) {
+ }
- final WrapperHandshakingClientHandshake wrapperHandshakingClientHandshake = new WrapperHandshakingClientHandshake(packetReceiveEvent);
+ // Block further logic.
+ return;
- // 'STATUS' only.
- if (wrapperHandshakingClientHandshake.getIntention() == WrapperHandshakingClientHandshake.ConnectionIntention.STATUS && wrapperHandshakingClientHandshake.getNextConnectionState() == ConnectionState.STATUS) {
+ }
- packetReceiveEvent.setCancelled(true);
+ // Request to get server's information (MOTD, version, player count).
+ if (packetTypeCommon == PacketType.Status.Client.REQUEST) {
- // Block further logic.
- return;
+ final boolean badHandshake = this.connectionState.getOrDefault(inetSocketAddress, null) != PacketType.Handshaking.Client.HANDSHAKE;
+ final boolean sendForgedMOTD = isBadPacket
+ || badHandshake
+ || (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage() && !this.corePlugin.getApiManager().isIpCached(ipAddress)) // Force forged MOTD for IPs that are not yet processed by the API.
+ || this.corePlugin.getApiManager().isIpCachedBlocked(ipAddress); // Send forged MOTD if the IP is blocked by the API.
- }
+ // Either suspicious port or bad handshake.
+ if (sendForgedMOTD) {
- }
+ // Custom logging based off port number.
+ //noinspection ExtractMethodRecommender
+ String reason = isInvalidPort
+ ? "Invalid port used by client."
+ : "Suspicious port used by client.";
- }
+ // Custom logging based off the factor (suspicious port/bad handshake).
+ reason = badHandshake
+ ? "Missing proper handshake."
+ : reason;
- }
+ // Custom logging based off if the protocol version is bad.
+ reason = isInvalidProtocol
+ ? "Invalid protocol version."
+ : reason;
- // Ran last so forged MOTD can be sent. This effectively only blocks actual connection packets.
- if (invalidPort) {
+ // Log the violation, report the IP to CoralGate API, cancel the packet and send forged MOTD.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason, false);
- // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
- logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid port used by client.");
+ // Send forged MOTD.
+ packetUser.sendPacketSilently(new WrapperStatusServerResponse(getForgedMOTD()));
+
+ }
// Block further logic.
return;
}
- /* Source port is ok, check IP now. */
+ // Ping to get the latency between the client and the server.
+ // Doesn't really expose any server information, but we'll keep it under control.
+ if (packetTypeCommon == PacketType.Status.Client.PING) {
- /*
- * API IP CHECK.
- */
+ final boolean badHandshake = this.connectionState.getOrDefault(inetSocketAddress, null) != PacketType.Handshaking.Client.HANDSHAKE;
+ final boolean sendForgedPong = isBadPacket
+ || badHandshake
+ || (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage() && !this.corePlugin.getApiManager().isIpCached(ipAddress)) // Force forged pong for IPs that are not yet processed by the API.
+ || this.corePlugin.getApiManager().isIpCachedBlocked(ipAddress); // Send forged pong if the IP is blocked by the API.
- // Match every login sequence related packet.
- final boolean isLoginSequencePacket = packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE
- || packetTypeCommon == PacketType.Login.Client.LOGIN_START
- || packetTypeCommon == PacketType.Login.Client.ENCRYPTION_RESPONSE
- || packetTypeCommon == PacketType.Login.Client.LOGIN_SUCCESS_ACK;
+ // Either suspicious port or bad handshake.
+ if (sendForgedPong) {
- if (isStatusPacket || isLoginSequencePacket) {
+ // Custom logging based off port number.
+ //noinspection ExtractMethodRecommender
+ String reason = isInvalidPort
+ ? "Invalid port used by client."
+ : "Suspicious port used by client.";
- if (this.corePlugin.getApiManager().isHealthy()) {
+ // Custom logging based off the factor (suspicious port/bad handshake).
+ reason = badHandshake
+ ? "Missing proper handshake."
+ : reason;
- this.corePlugin.getApiManager().isIpBlocked(ipAddress).thenAccept(blocked -> {
+ // Custom logging based off if the protocol version is bad.
+ reason = isInvalidProtocol
+ ? "Invalid protocol version."
+ : reason;
- if (blocked)
- // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
- logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "IP is blocked by the API.");
+ // Log the violation, report the IP to CoralGate API, cancel the packet and send forged pong.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason, false);
- });
+ // Send forged pong.
+ packetUser.sendPacketSilently(new WrapperStatusServerPong(new WrapperStatusClientPing(packetReceiveEvent).getTime()));
}
+ // Block further logic.
+ return;
+
}
- /*
- * PROTOCOL FILTERING.
- */
+ // Legacy ping exposes information like MOTD, version & player count.
+ // We have to use the NettyResponder to send packet a forged (or not) response.
+ if (packetTypeCommon == PacketType.Handshaking.Client.LEGACY_SERVER_LIST_PING) {
- if (isStatusPacket || isLoginSequencePacket) {
+ // Do not use "isBadPacket", it includes "isInvalidProtocol". Since we don't allow < 1.7 and this packet is for older netty-less versions, it will trigger "isInvalidProtocol".
+ // Simply use "isSuspiciousPort" part of "isBadPacket".
+ final boolean sendForgedPong = isSuspiciousPort
+ || (this.corePlugin.getConfigManager().getConfig().isAllowApiUsage() && !this.corePlugin.getApiManager().isIpCached(ipAddress)) // Force forged pong for IPs that are not yet processed by the API.
+ || this.corePlugin.getApiManager().isIpCachedBlocked(ipAddress); // Send forged pong if the IP is blocked by the API.
- // Exempt for local scanner test.
- final boolean scannerTest = inetSocketAddress.getHostString().equals("127.0.0.1") && inetSocketAddress.getPort() == 65535 && this.corePlugin.isTestMode();
- if (!scannerTest) {
+ // Suspicious port.
+ if (sendForgedPong) {
- // If the client's protocol version is invalid.
- if (packetReceiveEvent.getUser().getClientVersion() == null || packetReceiveEvent.getUser().getClientVersion().getProtocolVersion() == -1) {
+ // Custom logging based off port number.
+ final String reason = isInvalidPort
+ ? "Invalid port used by client."
+ : "Suspicious port used by client.";
- // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
- logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid protocol version.");
+ // Log the violation, report the IP to CoralGate API, cancel the packet and send forged response.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason, false);
+
+ // This is for older legacy pings.
+ final boolean isOldLegacyPing = ByteBufHelper.readableBytes(packetReceiveEvent.getByteBuf()) == 1;
- // Block further logic.
- return;
+ final NettyResponder nettyResponder = this.corePlugin.getNettyResponder();
+ // Send forged response using NettyResponder.
+ if (isOldLegacyPing) {
+ nettyResponder.sendOldLegacyPingResponse(packetUser, "A Minecraft Server", 0, 20);
+ } else {
+ nettyResponder.sendLegacyPingResponse(packetUser, 774, "1.21.11", "A Minecraft Server", 0, 20);
}
}
- }
-
- /*
- * PACKET ORDER FILTERING.
- */
-
- // Specific 'LOGIN' handshake login, the first packet in a legitimate connection sequence.
- if (packetTypeCommon == PacketType.Handshaking.Client.HANDSHAKE) {
-
- final WrapperHandshakingClientHandshake wrapperHandshakingClientHandshake = new WrapperHandshakingClientHandshake(packetReceiveEvent);
+ // Block further logic.
+ return;
- if (wrapperHandshakingClientHandshake.getIntention() == WrapperHandshakingClientHandshake.ConnectionIntention.LOGIN && wrapperHandshakingClientHandshake.getNextConnectionState() == ConnectionState.LOGIN) {
+ }
- this.connectionState.put(inetSocketAddress, packetTypeCommon);
+ // Handle invalid port incoming connections.
+ if (isInvalidPort) {
- }
+ // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Invalid port used by client.", true);
// Block further logic.
return;
}
- // Handshake has passed (or skipped).
+ // Login start procedure after handshake has been established.
if (packetTypeCommon == PacketType.Login.Client.LOGIN_START) {
- if (this.connectionState.getOrDefault(inetSocketAddress, null) != PacketType.Handshaking.Client.HANDSHAKE) {
+ // Validate state against certain conditions: previous HANDSHAKE.
+ // 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.Handshaking.Client.HANDSHAKE, packetTypeCommon, "Missing proper handshake.");
+
+ // This only works on Spigot/Paper due to a bug on BungeeCord/Velocity with packetevents.
+ // Proxies pass the API check when reaching "LOGIN_SUCCESS (S->C). It's the only place where it will work like LOGIN_START.
+ // Note: this does cause information about ENCRYPTION_REQUEST and SET_COMPRESSION to leak towards the player...
+ // See issues: https://github.com/GTeamX/CoralGate/issues/34 and https://github.com/retrooper/packetevents/issues/1465
+ // TODO: find better solution/fix PE issue
+ if (!PacketEvents.getAPI().getInjector().isProxy()) {
+
+ // The state validation failed, no need to run a check against the API.
+ if (packetReceiveEvent.isCancelled()) {
+ return; // Block further logic.
+ }
- // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
- logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Missing handshake procedure.");
+ // Either if health checking is disabled or if the API is truly healthy.
+ if (!this.corePlugin.getConfigManager().getConfig().isApiHealthCheck() || this.corePlugin.getApiManager().isHealthy()) {
- // Block further logic.
- return;
+ // Cancel the packet to send it later.
+ packetReceiveEvent.setCancelled(true);
- }
+ // Cache wrapper and data to reconstruct and send it back later.
+ final WrapperLoginClientLoginStart wrapperLoginClientLoginStart = new WrapperLoginClientLoginStart(packetReceiveEvent);
- final WrapperLoginClientLoginStart wrapperLoginClientLoginStart = new WrapperLoginClientLoginStart(packetReceiveEvent);
+ // Cache username, it's being used twice. Save some CPU for the rest of us!!!1111!!1!1!
+ final String username = wrapperLoginClientLoginStart.getUsername();
- // Filter debug usernames.
- if (wrapperLoginClientLoginStart.getUsername().startsWith("Player")) {
+ // Filter debug usernames.
+ if (username.startsWith("Player")) {
- // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
- logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Bot-like username pattern detected.");
+ // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Bot-like username pattern detected.", true);
- // Block further logic.
- return;
+ // The connection is closed and the packet dropped, no need to run a check against the API.
+ // Block further logic.
+ return;
- }
+ }
- this.connectionState.put(inetSocketAddress, packetTypeCommon);
+ // Fetch API async.
+ this.corePlugin.getApiManager().isIpBlocked(ipAddress).thenAccept(blocked -> {
+
+ if (blocked) {
+
+ // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "IP is blocked by the API.", true);
+
+ } else {
+
+ // Process the packet again, the player is verified by the API.
+ packetUser.receivePacketSilently(new WrapperLoginClientLoginStart(wrapperLoginClientLoginStart.getClientVersion(), username, wrapperLoginClientLoginStart.getSignatureData().orElse(null), wrapperLoginClientLoginStart.getPlayerUUID().orElse(null)));
+
+ }
+
+ });
+
+ }
+
+ }
// Block further logic.
return;
@@ -280,7 +397,7 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) {
// After login start has passed and the server sent an encryption request. This is only for servers that are in online mode.
if (packetTypeCommon == PacketType.Login.Client.ENCRYPTION_RESPONSE && this.corePlugin.isOnlineMode()) {
- // Validate state against certain conditions: previous ENCRYPTION_REQUEST
+ // Validate state against certain conditions: previous ENCRYPTION_REQUEST.
// 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.Login.Server.ENCRYPTION_REQUEST, packetTypeCommon, "Missing login start procedure.");
@@ -289,9 +406,10 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) {
}
+ // The client acknowledging the server sent LOGIN_SUCCESS. This only applies for 1.20.2+ clients and 1.20.2+ servers (if using Via).
if (packetTypeCommon == PacketType.Login.Client.LOGIN_SUCCESS_ACK) {
- // Validate state against certain conditions: previous LOGIN_SUCCESS
+ // Validate state against certain conditions: previous LOGIN_SUCCESS.
// 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.Login.Server.LOGIN_SUCCESS, packetTypeCommon, "Missing login success procedure.");
@@ -300,12 +418,22 @@ 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;
+ // Decide if we should use the new LOGIN_SUCCESS_ACK (1.20.2+) or the older LOGIN_SUCCESS (< 1.20.2).
+ // If we are on a backend, the server version is straight forward to get.
+ // However, if we're on a proxy, the server version is marked as 1.8 since it's the lowest Bungee supports (when it could be anything else).
+ // Therefore, we should base ourselves off the client's version ONLY if we are running on a proxy.
+ final boolean useLoginSuccessAck = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2)
+ || (PacketEvents.getAPI().getInjector().isProxy()
+ && packetUser.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_20_2));
+
+ final PacketTypeCommon expectedFinalState = useLoginSuccessAck
+ ? PacketType.Login.Client.LOGIN_SUCCESS_ACK
+ : PacketType.Login.Server.LOGIN_SUCCESS;
- if (this.connectionState.getOrDefault(inetSocketAddress, null) != expectedFinalState)
- logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Missing full connection procedure.");
+ // Connection procedure is not done yet, block incoming packets.
+ if (this.connectionState.getOrDefault(inetSocketAddress, null) != expectedFinalState) {
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Missing full connection procedure.", true);
+ }
/* All checks passed! */
@@ -315,35 +443,24 @@ public void onPacketReceive(final PacketReceiveEvent packetReceiveEvent) {
public void onPacketSend(final PacketSendEvent packetSendEvent) {
final InetSocketAddress inetSocketAddress = packetSendEvent.getSocketAddress();
- final String ipAddress = inetSocketAddress.getHostString();
// Exempt local IP addresses according to configuration file.
- if (this.corePlugin.getConfigManager().getConfig().isIgnoreLocalAddresses() && !this.corePlugin.isTestMode()) {
-
- try {
-
- final InetAddress inetAddress = InetAddress.getByName(ipAddress);
-
- if (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) return;
-
- } catch (final IOException ignored) {}
-
+ if (exemptLocalIpAddress(inetSocketAddress)) {
+ return;
}
+ final String ipAddress = inetSocketAddress.getHostString();
final PacketTypeCommon packetTypeCommon = packetSendEvent.getPacketType();
- /*
- * PACKET ORDER FILTERING.
- */
-
- // Whitelisted MOTD related packets.
- if (packetTypeCommon == PacketType.Status.Server.RESPONSE || packetTypeCommon == PacketType.Status.Server.PONG)
+ // Whitelisted packets.
+ if (packetTypeCommon == PacketType.Status.Server.RESPONSE || packetTypeCommon == PacketType.Status.Server.PONG) {
return;
+ }
// 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.");
@@ -352,16 +469,23 @@ public void onPacketSend(final PacketSendEvent packetSendEvent) {
}
- // Client should have sent the encryption response (if online mode). The compression level can be set to -1 (disabled) on proxies for less network/cpu overhead.
+ // Client should have sent the encryption response (if online mode).
+ // The compression level can be set to -1 (disabled) on proxies for less network/cpu overhead.
if (packetTypeCommon == PacketType.Login.Server.SET_COMPRESSION && this.corePlugin.getCompressionThreshold() >= 0) {
+ // Offline servers do not have encryption, the previous packet is therefore simply LOGIN_START.
final PacketTypeCommon requiredPacketTypeCommon = this.corePlugin.isOnlineMode()
? PacketType.Login.Client.ENCRYPTION_RESPONSE
: PacketType.Login.Client.LOGIN_START;
- // Validate state against certain conditions: previous ENCRYPTION_RESPONSE (if online mode) else LOGIN_START
+ // Determine proper message based off previous statement.
+ final String reason = requiredPacketTypeCommon == PacketType.Login.Client.ENCRYPTION_RESPONSE
+ ? "encryption response."
+ : "login start procedure.";
+
+ // 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.");
+ verifyAndTransitionState(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, requiredPacketTypeCommon, packetTypeCommon, "Missing " + reason);
// Block further logic.
return;
@@ -370,51 +494,203 @@ public void onPacketSend(final PacketSendEvent packetSendEvent) {
if (packetTypeCommon == PacketType.Login.Server.LOGIN_SUCCESS) {
+ // If it's an online server with compression: SET_COMPRESSION.
+ // If it's an online server WITHOUT compression: ENCRYPTION_RESPONSE.
+ //noinspection ExtractMethodRecommender
+ final PacketTypeCommon onlineModePreviousStatus = this.corePlugin.getCompressionThreshold() >= 0
+ ? PacketType.Login.Server.SET_COMPRESSION
+ : PacketType.Login.Client.ENCRYPTION_RESPONSE;
+
+ // If it's an offline server with compression: SET_COMPRESSION.
+ // If it's an offline server WITHOUT compression: LOGIN_START.
+ final PacketTypeCommon offlineModePreviousStatus = this.corePlugin.getCompressionThreshold() >= 0
+ ? PacketType.Login.Server.SET_COMPRESSION
+ : PacketType.Login.Client.LOGIN_START;
+
+ // Choose required packet type based off previous statements.
final PacketTypeCommon requiredPacketTypeCommon = this.corePlugin.isOnlineMode()
- ? (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);
+ ? onlineModePreviousStatus
+ : offlineModePreviousStatus;
- // 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.");
+ // This fix is really only for proxies. The API check run way earlier for the Spigot/Paper versions.
+ if (PacketEvents.getAPI().getInjector().isProxy()) {
+
+ // The state validation failed, no need to run a check against the API.
+ if (packetSendEvent.isCancelled()) {
+ return; // Block further logic.
+ }
+
+ // Either if health checking is disabled or if the API is truly healthy.
+ if (!this.corePlugin.getConfigManager().getConfig().isApiHealthCheck() || this.corePlugin.getApiManager().isHealthy()) {
+
+ // Cancel the packet to send it later.
+ packetSendEvent.setCancelled(true);
+
+ // Cache wrapper and data to reconstruct and send it back later.
+ final WrapperLoginServerLoginSuccess wrapperLoginServerLoginSuccess = new WrapperLoginServerLoginSuccess(packetSendEvent);
+
+ // Fetch API async.
+ this.corePlugin.getApiManager().isIpBlocked(ipAddress).thenAccept(blocked -> {
+
+ if (blocked) {
+
+ // Log the violation, report the IP to CoralGate API, cancel the packet and close the connection.
+ log(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, "IP is blocked by the API.");
+
+ } else {
+
+ // Process the packet again, the player is verified by the API.
+ packetSendEvent.getUser().sendPacketSilently(new WrapperLoginServerLoginSuccess(wrapperLoginServerLoginSuccess.getUserProfile(), wrapperLoginServerLoginSuccess.getSessionId(), wrapperLoginServerLoginSuccess.isStrictErrorHandling()));
+
+ }
+
+ });
+
+ }
+
+ }
+
// 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)
+ // Fired when a connection is closed.
+ // From the tests I've run, this gets triggered on Spigot/Paper (all the time) and BungeeCord ("Outdated server!").
+ if (packetTypeCommon == PacketType.Login.Server.DISCONNECT) {
+
+ final WrapperLoginServerDisconnect wrapperLoginServerDisconnect = new WrapperLoginServerDisconnect(packetSendEvent);
+
+ // Process DISCONNECT on a separate function, both PLAY, LOGIN and CONFIGURATION DISCONNECT share the same logic.
+ final Component safeDisconnectReason = processDisconnect(wrapperLoginServerDisconnect.getReason());
+
+ // Set the new reason and tell packetevents to re-encode the packet and send it.
+ wrapperLoginServerDisconnect.setReason(safeDisconnectReason);
+
+ packetSendEvent.markForReEncode(true);
+
+ // Block further logic.
+ return;
+
+ }
+
+ // Fired when a connection is closed.
+ // From the tests I've run, this gets triggered on Velocity ("Outdated client!") and BungeeCord ("Outdated client!").
+ if (packetTypeCommon == PacketType.Play.Server.DISCONNECT && PacketEvents.getAPI().getInjector().isProxy()) {
+
+ final WrapperPlayServerDisconnect wrapperPlayServerDisconnect = new WrapperPlayServerDisconnect(packetSendEvent);
+
+ // Process DISCONNECT on a separate function, both PLAY, LOGIN and CONFIGURATION DISCONNECT share the same logic.
+ final Component safeDisconnectReason = processDisconnect(wrapperPlayServerDisconnect.getReason());
+
+ // Set the new reason and tell packetevents to re-encode the packet and send it.
+ wrapperPlayServerDisconnect.setReason(safeDisconnectReason);
+
+ packetSendEvent.markForReEncode(true);
+
+ // Block further logic.
+ return;
+
+ }
+
+ // Fired when a connection is closed.
+ // From the tests I've run, this gets triggered on Velocity ("Outdated server!")
+ if (packetTypeCommon == PacketType.Configuration.Server.DISCONNECT && this.corePlugin.getPlatformProperties().getProperty("platform-name").equals("velocity")) {
+
+ final WrapperConfigServerDisconnect wrapperConfigServerDisconnect = new WrapperConfigServerDisconnect(packetSendEvent);
+
+ // Process DISCONNECT on a separate function, both PLAY, LOGIN and CONFIGURATION DISCONNECT share the same logic.
+ final Component safeDisconnectReason = processDisconnect(wrapperConfigServerDisconnect.getReason());
+
+ // Set the new reason and tell packetevents to re-encode the packet and send it.
+ wrapperConfigServerDisconnect.setReason(safeDisconnectReason);
+
+ packetSendEvent.markForReEncode(true);
+
+ // Block further logic.
+ return;
+
+ }
+
+ // Decide if we should use the new LOGIN_SUCCESS_ACK (1.20.2+) or the older LOGIN_SUCCESS (< 1.20.2).
+ // If we are on a backend, the server version is straight forward to get.
+ // However, if we're on a proxy, the server version is marked as 1.8 since it's the lowest Bungee supports (when it could be anything else).
+ // Therefore, we should base ourselves off the client's version ONLY if we are running on a proxy.
+ final boolean useLoginSuccessAck = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2)
+ || (PacketEvents.getAPI().getInjector().isProxy()
+ && packetSendEvent.getUser().getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_20_2));
+
+ final PacketTypeCommon expectedFinalState = useLoginSuccessAck
? PacketType.Login.Client.LOGIN_SUCCESS_ACK
: PacketType.Login.Server.LOGIN_SUCCESS;
- // Connection procedure is not done yet, block outgoing packets.
- if (this.connectionState.getOrDefault(inetSocketAddress, null) != requiredPacketTypeCommon)
- packetSendEvent.setCancelled(true);
+ // Connection procedure is not done yet, block incoming packets.
+ if (this.connectionState.getOrDefault(inetSocketAddress, null) != expectedFinalState) {
+ log(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, "Missing full connection procedure.");
+ }
/* All checks passed! */
}
- private void logAndClose(final PacketReceiveEvent packetReceiveEvent, final InetSocketAddress inetSocketAddress, final String ipAddress, final PacketTypeCommon packetTypeCommon, final String reason) {
+ @Override
+ public void onUserDisconnect(final UserDisconnectEvent userDisconnectEvent) {
+
+ final User user = userDisconnectEvent.getUser();
+ //noinspection ConstantValue
+ if (user == null || user.getAddress() == null) {
+ return;
+ }
+
+ // Clean up.
+ this.connectionState.remove(user.getAddress());
+
+ }
+
+ private void log(final PacketReceiveEvent packetReceiveEvent, final InetSocketAddress inetSocketAddress, final String ipAddress, final PacketTypeCommon packetTypeCommon, final String reason, final boolean closeConnection) {
+
+ final String messageComplement = closeConnection
+ ? " Closing connection from "
+ : " Dropping packet from ";
+
+ // Log based if the connection should be closed or not.
+ if (closeConnection) {
+ CorePlugin.getLogger().severe(reason + messageComplement + inetSocketAddress + ". [C->S | " + packetReceiveEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]");
+ } else {
+ CorePlugin.getLogger().warning(reason + messageComplement + inetSocketAddress + ". [C->S | " + packetReceiveEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]");
+ }
- CorePlugin.getLogger().severe(reason + " Closing connection from " + inetSocketAddress + ". [C->S | " + packetTypeCommon.getName() + "]");
- this.corePlugin.getApiManager().reportIp(ipAddress);
+ // Report IP to CoralGate's API.
+ this.corePlugin.getApiManager().checkIp(ipAddress);
packetReceiveEvent.setCancelled(true);
- packetReceiveEvent.getUser().closeConnection();
+ if (closeConnection) {
+ 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) {
+ private void log(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() + "]");
+ // Log based if the connection should be closed or not.
+ CorePlugin.getLogger().severe(reason + " Closing connection from " + inetSocketAddress + ". [S->C | " + packetSendEvent.getPacketType().getClass().getDeclaringClass().getSimpleName() + "." + packetTypeCommon.getName() + "]");
- this.corePlugin.getApiManager().reportIp(ipAddress);
+ // Report IP to CoralGate's API.
+ this.corePlugin.getApiManager().checkIp(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) {
@@ -423,7 +699,11 @@ private void verifyAndTransitionState(final PacketReceiveEvent packetReceiveEven
this.connectionState.put(inetSocketAddress, targetState);
- } else logAndClose(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason);
+ } else {
+
+ log(packetReceiveEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason, true);
+
+ }
}
@@ -433,12 +713,154 @@ private void verifyAndTransitionState(final PacketSendEvent packetSendEvent, fin
this.connectionState.put(inetSocketAddress, targetState);
- } else logAndClose(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason);
+ } else {
+
+ log(packetSendEvent, inetSocketAddress, ipAddress, packetTypeCommon, reason);
+
+ }
}
private String getForgedMOTD() {
- return "{\"description\":{\"text\":\"\",\"extra\":[\"A Minecraft Server\"]},\"players\":{\"max\":20,\"online\":0},\"version\":{\"name\":\"CraftBukkit 26.2\",\"protocol\":776},\"enforcesSecureChat\":true}";
+ return "{\"description\":{\"text\":\"\",\"extra\":[\"A Minecraft Server\"]},\"players\":{\"max\":20,\"online\":0},\"version\":{\"name\":\"Paper 1.21.11\",\"protocol\":774},\"enforcesSecureChat\":true}";
+ }
+
+ private boolean exemptLocalIpAddress(final InetSocketAddress inetSocketAddress) {
+
+ // Exempt local IP addresses according to configuration file.
+ if (this.corePlugin.getConfigManager().getConfig().isIgnoreLocalAddresses() && !this.corePlugin.isTestMode()) {
+
+ final InetAddress inetAddress = inetSocketAddress.getAddress();
+ return inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress();
+
+ }
+
+ return false;
+
+ }
+
+ private Component processDisconnect(final Component disconnectReason) {
+
+ if (disconnectReason instanceof TextComponent) {
+
+ final TextComponent disconnectReasonTextComponent = (TextComponent) disconnectReason;
+
+ // Spigot, Paper and Velocity work this way.
+ // BungeeCord loooooveeess to do it their way, we have it handle it specifically.
+ // TODO: directly take the kick message from spigot.yml if running on a backend.
+ if (!this.corePlugin.getPlatformProperties().getProperty("platform-name").equals("bungeecord")) {
+
+ // This MUST match every of the normal "Outdated...!" message.
+ // This ensures no server whatsoever can match this kind of kick message.
+ final Style style = disconnectReasonTextComponent.style();
+
+ // Determine expected color based on server type.
+ // If it's directly running on a backend, it's white (null) by default.
+ // On Velocity, it's red by default.
+ final NamedTextColor expectedNamedTextColor = (PacketEvents.getAPI().getInjector().isProxy()
+ ? NamedTextColor.RED
+ : null);
+
+ // Style must be clean/default and color must be exactly Red (#FF5555).
+ if (!isCleanStyle(style) || !Objects.equals(style.color(), expectedNamedTextColor)) {
+ return disconnectReason;
+ }
+
+ // Must have no children.
+ if (!disconnectReasonTextComponent.children().isEmpty()) {
+ return disconnectReason;
+ }
+
+ final String disconnectReasonString = disconnectReasonTextComponent.content();
+
+ // Determine regex based on server type.
+ // If it's directly running on a backend, it's safe to assume the server version is used in the kick message.
+ // However, on Velocity this isn't the case.
+ final String versionRegex = (PacketEvents.getAPI().getInjector().isProxy()
+ ? "\\d.*"
+ : PacketEvents.getAPI().getServerManager().getVersion().getReleaseName().replace(".", "\\.") + ".*");
+
+ // Use a regex to match any number and everything after.
+ // This ensures the version is completed changed no matter what's after.
+ // This helps for versions like "1.21.11 Unobfuscated".
+ final String newDisconnectReason = disconnectReasonString.replaceAll(versionRegex, "1.21.11");
+
+ // Reconstruct reason with spoofed server version.
+ return Component.text()
+ .content(newDisconnectReason)
+ .style(disconnectReasonTextComponent.style())
+ .build();
+
+
+ // Specific logic for BungeeCord nested kick messages.
+ } else {
+
+ // BungeeCord nested kick message layout handling
+ final List children = disconnectReasonTextComponent.children();
+
+ // A classic native kick structure requires the root to have exactly 2 children:
+ // Child 0: "Kicked whilst connecting to lobby: " (Red).
+ // Child 1: "Outdated client! Please use 1.20.1" (White).
+ if (children.size() != 2) {
+ return disconnectReason;
+ }
+
+ final Component kickMessage = children.get(0);
+ final Component outdatedMessage = children.get(1);
+
+ if (kickMessage instanceof TextComponent && outdatedMessage instanceof TextComponent) {
+
+ final TextComponent kickMessageTextComponent = (TextComponent) kickMessage;
+ final TextComponent outdatedMessageTextComponent = (TextComponent) outdatedMessage;
+
+ // This MUST match every of the normal "Outdated...!" message.
+ // This ensures no server whatsoever can match this kind of kick message.
+ final Style kickMessageStyle = kickMessageTextComponent.style();
+ final Style outdatedMessageStyle = outdatedMessageTextComponent.style();
+
+ // Style must be clean/default and color must be exactly Red (#FF5555).
+ if (!isCleanStyle(kickMessageStyle) || !NamedTextColor.RED.equals(kickMessageStyle.color())) {
+ return disconnectReason;
+ }
+
+ // Style must be clean/default and color must be exactly White (#FFFFFF).
+ if (!isCleanStyle(outdatedMessageStyle) || !NamedTextColor.WHITE.equals(outdatedMessageStyle.color())) {
+ return disconnectReason;
+ }
+
+ final String disconnectReasonString = outdatedMessageTextComponent.content();
+
+ // Use a regex to match any number and everything after.
+ // This ensures the version is completed changed no matter what's after.
+ // This helps for versions like "1.21.11 Unobfuscated".
+ final String newDisconnectReason = disconnectReasonString.replaceAll("\\d.*", "1.21.11");
+ final TextComponent newOutdatedMessageTextComponent = outdatedMessageTextComponent.content(newDisconnectReason);
+
+ // Reconstruct reason with spoofed server version.
+ return disconnectReasonTextComponent.children(Arrays.asList(kickMessageTextComponent, newOutdatedMessageTextComponent));
+
+ }
+
+ }
+
+ }
+
+ // In case something fails, but this is unsafe.
+ return disconnectReason;
+
+ }
+
+ // Made for less repetitiveness across the processDisconnect function.
+ private boolean isCleanStyle(final Style style) {
+ return style.decoration(TextDecoration.OBFUSCATED) == TextDecoration.State.NOT_SET
+ && style.decoration(TextDecoration.BOLD) == TextDecoration.State.NOT_SET
+ && style.decoration(TextDecoration.STRIKETHROUGH) == TextDecoration.State.NOT_SET
+ && style.decoration(TextDecoration.UNDERLINED) == TextDecoration.State.NOT_SET
+ && style.decoration(TextDecoration.ITALIC) == TextDecoration.State.NOT_SET
+ && style.clickEvent() == null
+ && style.hoverEvent() == null
+ && style.insertion() == null
+ && style.font() == null;
}
}
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..7d4af2d 100644
--- a/core/src/main/java/cloud/gteam/coralgate/update/UpdateChecker.java
+++ b/core/src/main/java/cloud/gteam/coralgate/update/UpdateChecker.java
@@ -26,9 +26,7 @@
import java.io.IOException;
import java.util.Objects;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
public class UpdateChecker {
@@ -49,15 +47,19 @@ public UpdateChecker(final CorePlugin corePlugin) {
.build());
}
- public CompletableFuture isUpToDate() {
+ private 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)
@@ -98,6 +100,22 @@ public CompletableFuture isUpToDate() {
}
+ public void checkForUpdates() {
+
+ CorePlugin.getLogger().info("Checking for updates, please wait...");
+
+ isUpToDate().thenAccept(upToDate -> {
+
+ if (upToDate) {
+ CorePlugin.getLogger().info("CoralGate is up to date!");
+ } else {
+ CorePlugin.getLogger().warning("You are behind updates on CoralGate! Latest version is '" + getLatestVersion() + "'. You are on '" + this.corePlugin.getPlatformProperties().getProperty("platform-version") + "'.");
+ }
+
+ });
+
+ }
+
public void shutdown() {
// Forcefully cancel any HTTP callbacks still hanging around.
@@ -106,7 +124,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) {
diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml
index 3b383ca..57f3cb3 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.3"
# Per category message prefixes.
prefixes:
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..bb434c0
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,51 @@
+[versions]
+coreVersion = "0.5.0"
+
+packetevents = "2.13.0"
+lamp = "4.0.0-rc.17"
+bstats = "3.2.1"
+
+jankson = "1.2.3"
+async-http-client = "2.16.0"
+jetbrains-annotations = "26.1.0"
+boosted-yaml = "1.3.7"
+gson = "2.14.0"
+adventure-api = "4.26.1"
+
+spigot = "1.8.8-R0.1-SNAPSHOT"
+bungeecord = "1.16-R0.4"
+velocity = "3.4.0"
+
+shadow = "9.6.1"
+
+[libraries]
+
+jankson = { module = "blue.endless:jankson", version.ref = "jankson" }
+async-http-client = { module = "org.asynchttpclient:async-http-client", version.ref = "async-http-client" }
+jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
+boosted-yaml = { module = "dev.dejvokep:boosted-yaml", version.ref = "boosted-yaml" }
+
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure-api" }
+
+bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
+bstats-bungeecord = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats" }
+bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
+
+lamp-common = { module = "io.github.revxrsal:lamp.common", version.ref = "lamp" }
+lamp-bukkit = { module = "io.github.revxrsal:lamp.bukkit", version.ref = "lamp" }
+lamp-bungee = { module = "io.github.revxrsal:lamp.bungee", version.ref = "lamp" }
+lamp-velocity = { module = "io.github.revxrsal:lamp.velocity", version.ref = "lamp" }
+lamp-brigadier = { module = "io.github.revxrsal:lamp.brigadier", version.ref = "lamp" }
+
+packetevents-api = { module = "com.github.retrooper:packetevents-api", version.ref = "packetevents" }
+packetevents-spigot = { module = "com.github.retrooper:packetevents-spigot", version.ref = "packetevents" }
+packetevents-bungeecord = { module = "com.github.retrooper:packetevents-bungeecord", version.ref = "packetevents" }
+packetevents-velocity = { module = "com.github.retrooper:packetevents-velocity", version.ref = "packetevents" }
+
+spigot-api = { module = "org.spigotmc:spigot-api", version.ref = "spigot" }
+bungeecord-api = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord" }
+velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" }
+
+[plugins]
+shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
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
deleted file mode 100644
index 9699f1e..0000000
--- a/paper/build.gradle.kts
+++ /dev/null
@@ -1,107 +0,0 @@
-plugins {
-
- `java-library`
- id("com.gradleup.shadow") version "9.4.2"
-
-}
-
-java {
-
- toolchain {
- languageVersion.set(JavaLanguageVersion.of(8))
- }
-
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
-
-}
-
-repositories {
-
- mavenCentral()
- mavenLocal()
-
- // PacketEvents repository.
- maven("https://repo.codemc.io/repository/maven-releases/")
-
- // Legacy Paper repository.
- maven("https://repo.papermc.io/repository/maven-snapshots/")
-
- // bungeecord-chat repository.
- maven("https://hub.spigotmc.org/nexus/content/groups/public/")
-
-}
-
-dependencies {
-
- // Get versions.
- val lampVersion: String by rootProject.extra
- val packetEventsVersion: String by rootProject.extra
- val bstatsVersion: String by rootProject.extra
-
- // Dependencies.
- implementation("org.bstats:bstats-bukkit:$bstatsVersion")
- implementation("io.github.revxrsal:lamp.common:$lampVersion")
- implementation("io.github.revxrsal:lamp.bukkit:$lampVersion")
-
- compileOnly("com.github.retrooper:packetevents-spigot:$packetEventsVersion")
- compileOnly("org.github.paperspigot:paperspigot-api:1.8.8-R0.1-SNAPSHOT")
-
- // Core implementation.
- implementation(project(":core"))
-
-}
-
-tasks.processResources {
-
- // Get versions.
- val packetEventsVersion: String by rootProject.extra
- val coreVersion: String by rootProject.extra
-
- // Replace plugin.yml
- filesMatching("plugin.yml") {
- expand("version" to project.version)
- }
-
- // Replace properties.
- filesMatching("platform.properties") {
-
- expand(
- "name" to project.name,
- "version" to project.version,
- "coreVersion" to coreVersion,
- "packeteventsVersion" to packetEventsVersion
- )
-
- }
-
-}
-
-tasks.shadowJar {
-
- // Wait for the core shadowJar to finish.
- dependsOn(project(":core").tasks.named("shadowJar"))
-
- archiveBaseName.set("CoralGate-Paper")
- archiveVersion.set(project.version.toString())
- archiveClassifier.set("")
-
- // Relocate Netty for AsyncHTTPClient.
- relocate("io.netty", "cloud.gteam.coralgate.libs.netty")
-
- // Relocate bStats.
- relocate("org.bstats", "cloud.gteam.coralgate.libs.bstats")
-
- exclude("META-INF/*.SF")
- exclude("META-INF/*.DSA")
- exclude("META-INF/*.RSA")
-
-}
-
-tasks.compileJava {
- dependsOn(project(":core").tasks.named("jar"))
-}
-
-tasks.build {
- dependsOn(tasks.shadowJar)
-}
diff --git a/paper/src/main/java/cloud/gteam/coralgate/PaperPlugin.java b/paper/src/main/java/cloud/gteam/coralgate/PaperPlugin.java
deleted file mode 100644
index be75297..0000000
--- a/paper/src/main/java/cloud/gteam/coralgate/PaperPlugin.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * This file is part of CoralGate - https://github.com/GTeamX/CoralGate
- * Copyright (C) 2026 GTeamX (GTeam) and it's 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 .
- */
-
-package cloud.gteam.coralgate;
-
-import cloud.gteam.coralgate.commands.CoralGateCommand;
-import cloud.gteam.coralgate.commands.PaperPermissionChecker;
-import cloud.gteam.coralgate.commands.permissions.PermissionFactory;
-import cloud.gteam.coralgate.processor.NetworkProcessor;
-import cloud.gteam.coralgate.utils.PlatformUtils;
-import com.github.retrooper.packetevents.PacketEvents;
-import com.github.retrooper.packetevents.event.PacketListenerPriority;
-import org.bstats.bukkit.Metrics;
-import org.bukkit.Bukkit;
-import org.bukkit.plugin.java.JavaPlugin;
-import revxrsal.commands.Lamp;
-import revxrsal.commands.bukkit.BukkitLamp;
-import revxrsal.commands.bukkit.actor.BukkitCommandActor;
-
-public final class PaperPlugin extends JavaPlugin {
-
- private final CorePlugin corePlugin = new CorePlugin();
-
- @Override
- public void onLoad() {
- PacketEvents.getAPI().getEventManager().registerListener(
- new NetworkProcessor(getCorePlugin()), PacketListenerPriority.HIGHEST);
- }
-
- @Override
- public void onEnable() {
-
- // Start bStats.
- new Metrics(this, 29439);
-
- // Load core.
- this.corePlugin.onEnable(this.getLogger(), getDataFolder(), Bukkit.getOnlineMode(), "server.properties", PlatformUtils.loadProperties(this.getClass()));
-
- // Load commands.
- final Lamp bukkitCommandActor = BukkitLamp.builder(this)
- .permissionFactory(new PermissionFactory(new PaperPermissionChecker()))
- .build();
- bukkitCommandActor.register(new CoralGateCommand(this.corePlugin));
-
- }
-
- @Override
- public void onDisable() {
-
- this.corePlugin.onDisable();
-
- }
-
- public CorePlugin getCorePlugin() {
- return this.corePlugin;
- }
-
-}
diff --git a/paper/src/main/resources/platform.properties b/paper/src/main/resources/platform.properties
deleted file mode 100644
index 91d4b12..0000000
--- a/paper/src/main/resources/platform.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-platform-name=${name}
-platform-version=${version}
-core-version=${coreVersion}
-packetevents-version=${packeteventsVersion}
\ No newline at end of file
diff --git a/paper/src/main/resources/plugin.yml b/paper/src/main/resources/plugin.yml
deleted file mode 100644
index 9476d7e..0000000
--- a/paper/src/main/resources/plugin.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-name: CoralGate
-version: "${version}"
-main: cloud.gteam.coralgate.PaperPlugin
-api-version: 1.13
-prefix: CoralGate
-load: STARTUP
-authors: [ XIII___, Vagdedes2 ]
-description: On-the-fly packet inspection and real-time IP verification for Minecraft servers and networks.
-depend: [packetevents]
\ No newline at end of file
diff --git a/scanner/src/Main.java b/scanner/src/Main.java
index e246ef4..9edb0a8 100644
--- a/scanner/src/Main.java
+++ b/scanner/src/Main.java
@@ -1,287 +1,547 @@
+// 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.io.*;
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.net.SocketTimeoutException;
+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_VERSION = "1.21.11";
+ private static final String FORGED_MARKER_1 = "Paper " + FORGED_VERSION;
+ private static final String FORGED_MARKER_2 = "\"protocol\":774";
+ 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]) : 774;
+ 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("Forged server data received as expected. " + info);
+ }
+ return Outcome.fail("Legit traffic wasn't sent forged motd on first request! motd=" + 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)) {
+ /** 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");
+ }
- System.out.println(GREEN + "PASSED! (filter active)" + RESET);
+ /** 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");
+ }
- } else {
+ /** 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 + ").");
+ }
+ }
- System.out.println(RED + "FAILED! (MOTD retrieved: " + jsonMOTD + ")" + RESET);
- isExposed = true;
+ /** 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 + ").");
+ }
+ }
- }
+ /** 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 + ").");
+ }
+ }
- } else {
+ /** 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);
- System.out.println(RED + "(!) Received unexpected Status packet ID: 0x" + Integer.toHexString(packetId) + RESET);
- isExposed = true;
+ if (resp == null) {
+ return Outcome.fail("Connection closed abruptly with no data, expected a forged disconnect reason packet.");
+ }
+ if (resp.id == 0x00) { // Disconnect Packet ID
+ String message = readString(new ByteArrayInputStream(resp.data));
+ if (message.contains(FORGED_VERSION)) {
+ return Outcome.pass("Bot rejected with expected forged string. Decoded text: \"" + message + "\"");
}
+ return Outcome.fail("Bot disconnected, but reason did not match forged version '" + FORGED_VERSION + "'! Got: \"" + message + "\"");
+ }
- } 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) {
+ return Outcome.fail("Bot-like username was not rejected! got packet id=" + resp.id
+ + " data=" + truncate(bytesToHex(resp.data)));
+ } catch (IOException e) {
+ return Outcome.fail("Connection threw unexpected exception instead of offering packet validation: " + e);
+ }
+ }
- System.out.println(RED + "FAILED! (server replied with login success)" + RESET);
- isExposed = true;
+ /** 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 + ").");
+ }
+ }
- } else {
+ /** 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);
- System.out.println(RED + "FAILED! (0x" + Integer.toHexString(packetId) + ")" + RESET);
- isExposed = true;
+ if (resp == null) {
+ return Outcome.fail("Connection closed abruptly with no data, expected a forged disconnect reason packet.");
+ }
+ if (resp.id == 0x00) { // Disconnect Packet ID
+ String message = readString(new ByteArrayInputStream(resp.data));
+ if (message.contains(FORGED_VERSION)) {
+ return Outcome.pass("Bot rejected with expected forged string on legit port. Decoded text: \"" + message + "\"");
}
-
+ return Outcome.fail("Bot disconnected, but reason did not match forged version '" + FORGED_VERSION + "'! Got: \"" + message + "\"");
}
- } 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 {
+ 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.fail("Connection threw unexpected exception instead of offering packet validation: " + e);
+ }
+ }
- // If it hasn't leaked yet, an abrupt drop indicates active proxy mitigation.
- System.out.println(GREEN + "PASSED! (zero bytes returned)" + RESET);
+ /** 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 + ").");
}
-
}
- interface PacketSender {
- void send(final DataOutputStream dataOutputStream) throws IOException;
+ // ----------------------------------------------------------------
+ // 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 + ").");
+ }
}
- 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());
-
+ 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 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());
-
+ 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 sendLoginStart(final DataOutputStream dataOutputStream, final String username) throws IOException {
-
- final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- final DataOutputStream loginStartOutputStream = new DataOutputStream(byteArrayOutputStream);
-
- loginStartOutputStream.writeByte(0x00);
-
- writeString(loginStartOutputStream, username);
-
- loginStartOutputStream.writeLong(0L);
- loginStartOutputStream.writeLong(1L);
-
- writePacket(dataOutputStream, byteArrayOutputStream.toByteArray());
-
+ 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;
}
- private static void writePacket(final DataOutputStream dataOutputStream, final byte[] data) throws IOException {
-
- 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
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8091328..1b667bf 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,7 +3,6 @@ rootProject.name = "CoralGate"
include(
"core",
"spigot",
- "paper",
"bungeecord",
"velocity"
)
diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts
index fa1cf90..0302c59 100644
--- a/spigot/build.gradle.kts
+++ b/spigot/build.gradle.kts
@@ -1,14 +1,14 @@
plugins {
`java-library`
- id("com.gradleup.shadow") version "9.4.2"
+ alias(libs.plugins.shadow)
}
java {
toolchain {
- languageVersion.set(JavaLanguageVersion.of(8))
+ languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -34,72 +34,63 @@ repositories {
dependencies {
- // Get versions.
- val lampVersion: String by rootProject.extra
- val packetEventsVersion: String by rootProject.extra
- val bstatsVersion: String by rootProject.extra
-
// Dependencies.
- implementation("org.bstats:bstats-bukkit:$bstatsVersion")
- implementation("io.github.revxrsal:lamp.common:$lampVersion")
- implementation("io.github.revxrsal:lamp.bukkit:$lampVersion")
+ implementation(libs.bstats.bukkit)
+ implementation(libs.lamp.common)
+ implementation(libs.lamp.bukkit)
- compileOnly("com.github.retrooper:packetevents-spigot:$packetEventsVersion")
- compileOnly("org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT")
+ compileOnly(libs.packetevents.spigot)
+ compileOnly(libs.spigot.api)
// Core implementation.
- implementation(project(":core"))
+ compileOnly(project(":core"))
}
tasks.processResources {
- // Get versions.
- val packetEventsVersion: String by rootProject.extra
- val coreVersion: String by rootProject.extra
+ inputs.property("name", project.name)
+ inputs.property("version", project.version)
+ inputs.property("coreVersion", libs.versions.coreVersion.get())
+ inputs.property("packeteventsVersion", libs.versions.packetevents.get())
- // Replace plugin.yml
- filesMatching("plugin.yml") {
- expand("version" to project.version)
+ // Replaces placeholders.
+ filesMatching(listOf("plugin.yml", "paper-plugin.yml", "platform.properties")) {
+ expand(inputs.properties)
}
- // Replace properties.
- filesMatching("platform.properties") {
-
- expand(
- "name" to project.name,
- "version" to project.version,
- "coreVersion" to coreVersion,
- "packeteventsVersion" to packetEventsVersion
- )
+}
- }
+// A trick so netty used by async-http-client is always
+// relocated, but netty used by injector is always provided by platform (spigot, bungeecord, velocity)
+val coreProvider = provider { project(":core").tasks.shadowJar.flatMap { it.archiveFile } }
+tasks.jar {
+ enabled = false // only shadowJar is used
}
tasks.shadowJar {
// Wait for the core shadowJar to finish.
- dependsOn(project(":core").tasks.named("shadowJar"))
+ dependsOn(":core:shadowJar")
- archiveBaseName.set("CoralGate-Spigot")
- archiveVersion.set(project.version.toString())
- archiveClassifier.set("")
+ archiveBaseName = "CoralGate-Spigot"
+ archiveVersion = project.version.toString()
+ archiveClassifier = ""
+
+ from(zipTree(coreProvider)) // include shadowed core
// Relocate bStats.
relocate("org.bstats", "cloud.gteam.coralgate.libs.bstats")
- // Relocate netty.
- relocate("io.netty", "cloud.gteam.coralgate.libs.netty")
-
exclude("META-INF/*.SF")
exclude("META-INF/*.DSA")
exclude("META-INF/*.RSA")
-}
+ filesMatching("META-INF/*.kotlin_module") {
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+ }
-tasks.compileJava {
- dependsOn(project(":core").tasks.named("jar"))
}
tasks.build {
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/SpigotPlugin.java b/spigot/src/main/java/cloud/gteam/coralgate/SpigotPlugin.java
index b48d982..789888a 100644
--- a/spigot/src/main/java/cloud/gteam/coralgate/SpigotPlugin.java
+++ b/spigot/src/main/java/cloud/gteam/coralgate/SpigotPlugin.java
@@ -21,6 +21,8 @@
import cloud.gteam.coralgate.commands.CoralGateCommand;
import cloud.gteam.coralgate.commands.SpigotPermissionChecker;
import cloud.gteam.coralgate.commands.permissions.PermissionFactory;
+import cloud.gteam.coralgate.injector.SpigotInjector;
+import cloud.gteam.coralgate.injector.handlers.SpigotNettyResponder;
import cloud.gteam.coralgate.processor.NetworkProcessor;
import cloud.gteam.coralgate.utils.PlatformUtils;
import com.github.retrooper.packetevents.PacketEvents;
@@ -32,24 +34,50 @@
import revxrsal.commands.bukkit.BukkitLamp;
import revxrsal.commands.bukkit.actor.BukkitCommandActor;
+import java.util.Properties;
+
public final class SpigotPlugin extends JavaPlugin {
private final CorePlugin corePlugin = new CorePlugin();
+ private final SpigotInjector injector = new SpigotInjector();
+
@Override
public void onLoad() {
- PacketEvents.getAPI().getEventManager().registerListener(
- new NetworkProcessor(getCorePlugin()), PacketListenerPriority.HIGHEST);
+
+ if (!this.injector.isServerBound()) {
+ this.injector.inject();
+ }
+
+ PacketEvents.getAPI().getEventManager().registerListener(new NetworkProcessor(getCorePlugin()), PacketListenerPriority.HIGHEST);
+
}
@Override
public void onEnable() {
+ // Inject for LEGACY_SERVER_LIST_PING.
+ if (!this.injector.hasInjected) {
+ this.injector.inject();
+ }
+
// Start bStats.
new Metrics(this, 29439);
+ // Get properties early to modify the platform name later on if needed.
+ final Properties platformProperties = PlatformUtils.loadProperties(this.getClass());
+
+ // Paper's bootstrapper context class is only present when loaded via paper-plugin.yml
+ // If it exists we're running as 'paper'.
+ try {
+
+ Class.forName("io.papermc.paper.plugin.bootstrap.BootstrapContext");
+ platformProperties.setProperty("platform-name", "paper");
+
+ } catch (final ClassNotFoundException ignored) {}
+
// Load core.
- this.corePlugin.onEnable(this.getLogger(), getDataFolder(), Bukkit.getOnlineMode(), "server.properties", PlatformUtils.loadProperties(this.getClass()));
+ this.corePlugin.onEnable(this.getLogger(), getDataFolder(), Bukkit.getOnlineMode(), "server.properties", platformProperties, new SpigotNettyResponder());
// Load commands.
final Lamp bukkitCommandActor = BukkitLamp.builder(this)
@@ -62,6 +90,8 @@ public void onEnable() {
@Override
public void onDisable() {
+ this.injector.uninject();
+
this.corePlugin.onDisable();
}
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/injector/SpigotInjector.java b/spigot/src/main/java/cloud/gteam/coralgate/injector/SpigotInjector.java
new file mode 100644
index 0000000..765a123
--- /dev/null
+++ b/spigot/src/main/java/cloud/gteam/coralgate/injector/SpigotInjector.java
@@ -0,0 +1,169 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector;
+
+import cloud.gteam.coralgate.CorePlugin;
+import cloud.gteam.coralgate.injector.connection.ServerChannelHandler;
+import com.github.retrooper.packetevents.util.reflection.ReflectionObject;
+import io.github.retrooper.packetevents.util.InjectedList;
+import io.github.retrooper.packetevents.util.SpigotReflectionUtil;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelPipeline;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class SpigotInjector {
+
+ public static final String DECODER_NAME = "cg-decoder";
+ public static final String CONNECTION_HANDLER_NAME = "cg-connection-handler";
+ public static final String SERVER_CHANNEL_HANDLER_NAME = "cg-connection-initializer";
+
+ // Channels that process connecting clients.
+ public final Set injectedConnectionChannels = new HashSet<>();
+ private int connectionChannelsListIndex = -1;
+
+ public boolean hasInjected = false;
+
+ public boolean isServerBound() {
+
+ // We want to check if the server has been bound to the port already.
+ final Object serverConnection = SpigotReflectionUtil.getMinecraftServerConnectionInstance();
+ if (serverConnection != null) {
+
+ final ReflectionObject reflectServerConnection = new ReflectionObject(serverConnection);
+
+ // There should only be 2 lists.
+ for (int i = 0; i < 2; i++) {
+
+ final List> list = reflectServerConnection.readList(i);
+ for (final Object value : list) {
+
+ if (value instanceof ChannelFuture) {
+
+ this.connectionChannelsListIndex = i;
+ // Found the right list.
+ // It has connection channels, so the server has been bound.
+ return true;
+
+ }
+
+ }
+
+ }
+
+ }
+
+ return false;
+
+ }
+
+ public void inject() {
+
+ final Object serverConnection = SpigotReflectionUtil.getMinecraftServerConnectionInstance();
+ if (serverConnection != null) {
+
+ final ReflectionObject reflectServerConnection = new ReflectionObject(serverConnection);
+ final List connectionChannelFutures = reflectServerConnection.readList(this.connectionChannelsListIndex);
+ final InjectedList wrappedList = new InjectedList<>(connectionChannelFutures, future -> {
+
+ // Each time a channel future is added, we run this.
+ // This is automatically also ran for the elements already added before we wrapped the list.
+ final Channel channel = future.channel();
+ // Inject into the server connection channel.
+ injectServerChannel(channel);
+ // Make sure to store it, so we can uninject later on.
+ this.injectedConnectionChannels.add(channel);
+
+ });
+
+ // Replace the list with our wrapped one.
+ reflectServerConnection.writeList(this.connectionChannelsListIndex, wrappedList);
+
+ this.hasInjected = true;
+
+ }
+
+ }
+
+ public void uninject() {
+
+ // Uninject our connection handler from these connection channels.
+ for (final Channel connectionChannel : this.injectedConnectionChannels) {
+ uninjectServerChannel(connectionChannel);
+ }
+
+ this.injectedConnectionChannels.clear();
+
+ final Object serverConnection = SpigotReflectionUtil.getMinecraftServerConnectionInstance();
+ if (serverConnection != null) {
+
+ final ReflectionObject reflectServerConnection = new ReflectionObject(serverConnection);
+ final List connectionChannelFutures = reflectServerConnection.readList(this.connectionChannelsListIndex);
+ if (connectionChannelFutures instanceof InjectedList) {
+
+ // Let us unwrap this. We no longer want to listen to connecting channels.
+ reflectServerConnection.writeList(this.connectionChannelsListIndex, ((InjectedList) connectionChannelFutures).originalList());
+
+ }
+
+ }
+
+ }
+
+ private void injectServerChannel(final Channel serverChannel) {
+
+ final ChannelPipeline pipeline = serverChannel.pipeline();
+ if (pipeline.get(SpigotInjector.CONNECTION_HANDLER_NAME) != null) {
+
+ // Why does it already exist? Remove it.
+ pipeline.remove(SpigotInjector.CONNECTION_HANDLER_NAME);
+
+ }
+
+ // Make sure we handle connections after ProtocolSupport.
+ if (pipeline.get("SpigotNettyServerChannelHandler#0") != null) {
+ pipeline.addAfter("SpigotNettyServerChannelHandler#0", SpigotInjector.CONNECTION_HANDLER_NAME, new ServerChannelHandler());
+ }
+
+ // Make sure we handle connections after Geyser.
+ else if (pipeline.get("floodgate-init") != null) {
+ pipeline.addAfter("floodgate-init", SpigotInjector.CONNECTION_HANDLER_NAME, new ServerChannelHandler());
+ } else if (pipeline.get("MinecraftPipeline#0") != null) { // Some forks add a handler which adds the other necessary vanilla handlers like (decoder, encoder, etc...)
+ pipeline.addAfter("MinecraftPipeline#0", SpigotInjector.CONNECTION_HANDLER_NAME, new ServerChannelHandler());
+ } else { // Otherwise, make sure we are first.
+ pipeline.addFirst(SpigotInjector.CONNECTION_HANDLER_NAME, new ServerChannelHandler());
+ }
+
+ }
+
+ private void uninjectServerChannel(final Channel serverChannel) {
+
+ if (serverChannel.pipeline().get(SpigotInjector.CONNECTION_HANDLER_NAME) != null) {
+ serverChannel.pipeline().remove(SpigotInjector.CONNECTION_HANDLER_NAME);
+ } else {
+ CorePlugin.getLogger().warning("Failed to uninject server channel, handler not found");
+ }
+
+ }
+
+}
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/PreChannelInitializer_v1_12.java b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/PreChannelInitializer_v1_12.java
new file mode 100644
index 0000000..7693e69
--- /dev/null
+++ b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/PreChannelInitializer_v1_12.java
@@ -0,0 +1,53 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import com.github.retrooper.packetevents.protocol.ConnectionState;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+public class PreChannelInitializer_v1_12 extends ChannelInboundHandlerAdapter {
+
+ @Override
+ public void channelRegistered(final ChannelHandlerContext ctx) {
+
+ try {
+ ServerConnectionInitializer.initChannel(ctx.channel(), ConnectionState.HANDSHAKING);
+ } catch (final Throwable t) {
+
+ InternalLoggerFactory.getInstance(ChannelInitializer.class).warn("Failed to initialize a channel. Closing: " + ctx.channel(), t);
+ ctx.close();
+
+ } finally {
+
+ final ChannelPipeline pipeline = ctx.pipeline();
+ if (pipeline.context(this) != null) {
+ pipeline.remove(this);
+ }
+
+ }
+
+ ctx.pipeline().fireChannelRegistered();
+
+ }
+
+}
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/PreChannelInitializer_v1_8.java b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/PreChannelInitializer_v1_8.java
new file mode 100644
index 0000000..b9ba106
--- /dev/null
+++ b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/PreChannelInitializer_v1_8.java
@@ -0,0 +1,41 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import com.github.retrooper.packetevents.protocol.ConnectionState;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+
+public class PreChannelInitializer_v1_8 extends ChannelInitializer {
+
+ @Override
+ protected void initChannel(final Channel channel) {
+
+ channel.pipeline().addLast(new ChannelInitializer() {
+
+ @Override
+ protected void initChannel(final Channel channel) {
+ ServerConnectionInitializer.initChannel(channel, ConnectionState.HANDSHAKING);
+ }
+
+ });
+
+ }
+
+}
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/ServerChannelHandler.java b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/ServerChannelHandler.java
new file mode 100644
index 0000000..3f29715
--- /dev/null
+++ b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/ServerChannelHandler.java
@@ -0,0 +1,98 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import cloud.gteam.coralgate.injector.SpigotInjector;
+import com.github.retrooper.packetevents.util.PEVersion;
+import io.github.retrooper.packetevents.util.SpigotReflectionUtil;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.util.Version;
+
+import java.util.Map;
+
+import static io.github.retrooper.packetevents.injector.connection.ServerChannelHandler.*;
+
+public class ServerChannelHandler extends ChannelInboundHandlerAdapter {
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+
+ if (!(msg instanceof Channel)) {
+ return;
+ }
+
+ final Channel channel = (Channel) msg;
+
+ // Resolve netty version only once.
+ if (NETTY_VERSION == null && !CHECKED_NETTY_VERSION) {
+
+ NETTY_VERSION = resolveNettyVersion();
+ CHECKED_NETTY_VERSION = true;
+
+ }
+
+ // Depends on netty version. If we cannot resolve that we just check server version.
+ if ((NETTY_VERSION != null && NETTY_VERSION.isNewerThan(MODERN_NETTY_VERSION))
+ || SpigotReflectionUtil.V_1_12_OR_HIGHER) {
+ channel.pipeline().addLast(SpigotInjector.SERVER_CHANNEL_HANDLER_NAME, new PreChannelInitializer_v1_12());
+ } else {
+ channel.pipeline().addFirst(SpigotInjector.SERVER_CHANNEL_HANDLER_NAME, new PreChannelInitializer_v1_8());
+ }
+
+ super.channelRead(ctx, msg);
+
+ }
+
+ private static PEVersion resolveNettyVersion() {
+
+ final Map nettyArtifacts = Version.identify();
+
+ Version version = nettyArtifacts.getOrDefault("netty-common", nettyArtifacts.get("netty-all"));
+
+ if (version == null && !nettyArtifacts.isEmpty()) {
+ version = nettyArtifacts.values().iterator().next();
+ }
+
+ if (version != null) {
+
+ String stringVersion = version.artifactVersion();
+
+ // Remove the ".Final" from the version by just removing any words (non numbers or dots).
+ stringVersion = stringVersion.replaceAll("[^\\d.]", "");
+
+ // Make sure stringVersion only contains 3 values like 4.2.0 but not 4.2.0.2.
+ final String[] splitVersion = stringVersion.split("\\.");
+ if (splitVersion.length > 3) {
+ stringVersion = splitVersion[0] + "." + splitVersion[1] + "." + splitVersion[2];
+ }
+
+ // If the string ends with a dot, remove it.
+ stringVersion = stringVersion.endsWith(".") ? stringVersion.substring(0, stringVersion.length() - 1) : stringVersion;
+
+ return PEVersion.fromString(stringVersion);
+
+ }
+
+ return null;
+
+ }
+
+}
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java
new file mode 100644
index 0000000..4ff69ac
--- /dev/null
+++ b/spigot/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java
@@ -0,0 +1,54 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import cloud.gteam.coralgate.injector.SpigotInjector;
+import cloud.gteam.coralgate.injector.handlers.SpigotDecoder;
+import com.github.retrooper.packetevents.netty.channel.ChannelHelper;
+import com.github.retrooper.packetevents.protocol.ConnectionState;
+import com.github.retrooper.packetevents.protocol.player.User;
+import com.github.retrooper.packetevents.protocol.player.UserProfile;
+import com.github.retrooper.packetevents.util.FakeChannelUtil;
+import io.netty.channel.Channel;
+
+import java.util.NoSuchElementException;
+
+public class ServerConnectionInitializer {
+
+ public static void initChannel(final Channel ch, final ConnectionState connectionState) {
+
+ if (FakeChannelUtil.isFakeChannel(ch)) {
+ return;
+ }
+
+ relocateHandlers(ch, new User(ch, connectionState, null, new UserProfile(null, null)));
+
+ }
+
+ public static void relocateHandlers(final Channel ctx, final User user) {
+
+ try {
+ ctx.pipeline().addBefore("legacy_query", SpigotInjector.DECODER_NAME, new SpigotDecoder(user));
+ } catch (final NoSuchElementException e) {
+ throw new IllegalStateException("CoralGateInjector failed to add a decoder to the netty pipeline. Pipeline handlers: " + ChannelHelper.pipelineHandlerNamesAsString(ctx), e);
+ }
+
+ }
+
+}
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/injector/handlers/SpigotDecoder.java b/spigot/src/main/java/cloud/gteam/coralgate/injector/handlers/SpigotDecoder.java
new file mode 100644
index 0000000..940ce69
--- /dev/null
+++ b/spigot/src/main/java/cloud/gteam/coralgate/injector/handlers/SpigotDecoder.java
@@ -0,0 +1,192 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.handlers;
+
+import com.github.retrooper.packetevents.PacketEvents;
+import com.github.retrooper.packetevents.event.PacketReceiveEvent;
+import com.github.retrooper.packetevents.event.simple.PacketHandshakeReceiveEvent;
+import com.github.retrooper.packetevents.exception.PacketProcessException;
+import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
+import com.github.retrooper.packetevents.protocol.player.User;
+import com.github.retrooper.packetevents.util.ExceptionUtil;
+import io.github.retrooper.packetevents.util.SpigotReflectionUtil;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageDecoder;
+
+import java.util.List;
+
+public class SpigotDecoder extends MessageToMessageDecoder {
+
+ public final User user;
+
+ public SpigotDecoder(final User user) {
+ this.user = user;
+ }
+
+ public void read(final ChannelHandlerContext ctx, final ByteBuf input, final List out) {
+
+ try {
+
+ handleServerBoundPacket(ctx.channel(), this.user, input);
+ out.add(ByteBufHelper.retain(input));
+
+ } catch (final Throwable t) {
+
+ // We must be sure all the exceptions caused by our handlers are PacketProcessExceptions.
+ // In the case we have thrown an exception that is not a PacketProcessException, let's wrap it in order to
+ // allow exceptionCaught to handle it properly.
+ if (ExceptionUtil.isException(t, PacketProcessException.class)) {
+ throw t;
+ } else {
+ throw new PacketProcessException(t);
+ }
+
+ }
+
+ }
+
+ public static void handleServerBoundPacket(final Object channel, final User user, final ByteBuf buffer) {
+
+ final int preProcessIndex = ByteBufHelper.readerIndex(buffer);
+ final int readable = ByteBufHelper.readableBytes(buffer);
+ boolean isLegacyServerListPing = false;
+
+ // Detect every legacy server list ping variant by peeking bytes directly (never consuming
+ // the reader index), rather than parsing a VarInt. A VarInt read would throw on the
+ // single-byte pre-1.4 variant since there's no second byte to complete it.
+ if (readable >= 1 && ByteBufHelper.getUnsignedByte(buffer, preProcessIndex) == 0xFE) {
+
+ // Bare single 0xFE, nothing trailing, pre-1.4 clients (1.1, 1.2, 1.3).
+ if (readable == 1) {
+
+ isLegacyServerListPing = true;
+
+ } else if (ByteBufHelper.getUnsignedByte(buffer, preProcessIndex + 1) == 0x01) {
+
+ if (readable == 2) {
+
+ // FE 01, nothing trailing, 1.4/1.5 clients.
+ isLegacyServerListPing = true;
+
+ } else if (readable >= 3 && ByteBufHelper.getUnsignedByte(buffer, preProcessIndex + 2) == 0xFA) {
+
+ // FE 01 FA ..., 1.6 clients.
+ isLegacyServerListPing = true;
+
+ }
+
+ }
+
+ }
+
+ if (!isLegacyServerListPing) {
+ return;
+ }
+
+ final PacketReceiveEvent packetReceiveEvent = new PacketHandshakeReceiveEvent(channel, user, null, buffer, true);
+
+ PacketEvents.getAPI().getEventManager().callEvent(packetReceiveEvent, () -> ByteBufHelper.readerIndex(buffer, ByteBufHelper.readerIndex(buffer)));
+
+ if (!packetReceiveEvent.isCancelled()) {
+
+ // Did they ever use a wrapper?
+ if (packetReceiveEvent.getLastUsedWrapper() != null) {
+
+ // Rewrite the buffer.
+ ByteBufHelper.clear(buffer);
+ packetReceiveEvent.getLastUsedWrapper().writeVarInt(packetReceiveEvent.getPacketId());
+ packetReceiveEvent.getLastUsedWrapper().write();
+
+ } else {
+
+ // If no wrappers were used, just pass on the original buffer.
+ // Correct the reader index, basically what the next handler is expecting.
+ ByteBufHelper.readerIndex(buffer, preProcessIndex);
+
+ }
+
+ } else {
+
+ // Cancelling the packet, lets clear the buffer.
+ ByteBufHelper.clear(buffer);
+
+ }
+
+ if (packetReceiveEvent.hasPostTasks()) {
+
+ for (final Runnable task : packetReceiveEvent.getPostTasks()) {
+ task.run();
+ }
+
+ }
+
+ }
+
+ @Override
+ public void decode(final ChannelHandlerContext ctx, final ByteBuf buffer, final List out) throws Exception {
+
+ if (buffer.isReadable()) {
+ read(ctx, buffer, out);
+ }
+
+ }
+
+ @Override
+ public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
+
+ // If we didn't cause the exception, let the server handle it.
+ if (!ExceptionUtil.isException(cause, PacketProcessException.class)) {
+
+ super.exceptionCaught(ctx, cause);
+ return;
+
+ }
+
+ // We log exceptions only if the server is in debug mode.
+ if (PacketEvents.getAPI().getSettings().isDebugEnabled() || SpigotReflectionUtil.isMinecraftServerInstanceDebugging()) {
+
+ if (PacketEvents.getAPI().getSettings().isFullStackTraceEnabled()) {
+
+ final String state = this.user != null ? this.user.getDecoderState().name() : "null";
+ final String clientVersion = this.user != null ? this.user.getClientVersion().getReleaseName() : "null";
+
+ PacketEvents.getAPI().getLogManager().warn("An error occurred while processing a packet from "
+ + this.user.getProfile().getName() + " (state: " + state + ", clientVersion: " + clientVersion +
+ ", serverVersion: " + PacketEvents.getAPI().getServerManager().getVersion().getReleaseName() + ")", cause);
+
+ } else {
+ PacketEvents.getAPI().getLogManager().warn(cause.getMessage());
+ }
+
+ }
+
+ if (PacketEvents.getAPI().getSettings().isKickOnPacketExceptionEnabled()) {
+
+ ctx.channel().close();
+
+ if (this.user != null && this.user.getProfile().getName() != null) {
+ PacketEvents.getAPI().getLogManager().warn("Disconnected " + this.user.getProfile().getName() + " due to an invalid packet!");
+ }
+
+ }
+
+ }
+
+}
diff --git a/spigot/src/main/java/cloud/gteam/coralgate/injector/handlers/SpigotNettyResponder.java b/spigot/src/main/java/cloud/gteam/coralgate/injector/handlers/SpigotNettyResponder.java
new file mode 100644
index 0000000..f80f452
--- /dev/null
+++ b/spigot/src/main/java/cloud/gteam/coralgate/injector/handlers/SpigotNettyResponder.java
@@ -0,0 +1,70 @@
+/*
+ * This file is part of CoralGate - https://github.com/GTeamX/CoralGate
+ * Copyright (C) 2026 GTeamX (GTeam) and it's 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 .
+ */
+
+package cloud.gteam.coralgate.injector.handlers;
+
+import cloud.gteam.coralgate.injector.NettyResponder;
+import com.github.retrooper.packetevents.protocol.player.User;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+
+import java.nio.charset.StandardCharsets;
+
+public class SpigotNettyResponder implements NettyResponder {
+
+ public SpigotNettyResponder() {}
+
+ @Override
+ public void sendLegacyPingResponse(final User user, final int protocolVersion, final String serverVersion, final String motd, final int onlinePlayers, final int maxPlayers) {
+
+ final String response = "§1\0"
+ + protocolVersion + "\0"
+ + serverVersion + "\0"
+ + motd + "\0"
+ + onlinePlayers + "\0"
+ + maxPlayers;
+ write(user, response);
+
+ }
+
+ @Override
+ public void sendOldLegacyPingResponse(final User user, final String motd, final int onlinePlayers, final int maxPlayers) {
+
+ final String response = motd + "§" + onlinePlayers + "§" + maxPlayers;
+ write(user, response);
+
+ }
+
+ private void write(final User user, final String response) {
+
+ final Channel channel = (Channel) user.getChannel();
+ final byte[] responseBytes = response.getBytes(StandardCharsets.UTF_16BE);
+
+ final ByteBuf buf = channel.alloc().buffer();
+ buf.writeByte(0xFF);
+ buf.writeShort(response.length());
+ buf.writeBytes(responseBytes);
+
+ channel.pipeline().firstContext()
+ .writeAndFlush(buf)
+ .addListener(ChannelFutureListener.CLOSE);
+
+ }
+
+}
diff --git a/paper/src/main/resources/paper-plugin.yml b/spigot/src/main/resources/paper-plugin.yml
similarity index 83%
rename from paper/src/main/resources/paper-plugin.yml
rename to spigot/src/main/resources/paper-plugin.yml
index 3da7d68..5094c47 100644
--- a/paper/src/main/resources/paper-plugin.yml
+++ b/spigot/src/main/resources/paper-plugin.yml
@@ -1,7 +1,7 @@
name: CoralGate
version: "${version}"
-main: cloud.gteam.coralgate.PaperPlugin
-api-version: 1.21
+main: cloud.gteam.coralgate.SpigotPlugin
+api-version: '1.19'
prefix: CoralGate
load: STARTUP
authors: [ XIII___, Vagdedes2 ]
diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml
index cd6191a..18a5437 100644
--- a/spigot/src/main/resources/plugin.yml
+++ b/spigot/src/main/resources/plugin.yml
@@ -6,5 +6,4 @@ prefix: CoralGate
load: STARTUP
authors: [ XIII___, Vagdedes2 ]
description: On-the-fly packet inspection and real-time IP verification for Minecraft servers and networks.
-depend:
- - packetevents
+depend: [ packetevents ]
diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts
index 685a2de..842ce22 100644
--- a/velocity/build.gradle.kts
+++ b/velocity/build.gradle.kts
@@ -1,14 +1,14 @@
plugins {
`java-library`
- id("com.gradleup.shadow") version "9.4.2"
+ alias(libs.plugins.shadow)
}
java {
toolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
+ languageVersion = JavaLanguageVersion.of(17)
}
sourceCompatibility = JavaVersion.VERSION_17
@@ -31,58 +31,53 @@ repositories {
dependencies {
- // Get versions.
- val lampVersion: String by rootProject.extra
- val packetEventsVersion: String by rootProject.extra
- val bstatsVersion: String by rootProject.extra
-
// Dependencies.
- implementation("org.bstats:bstats-velocity:$bstatsVersion")
- implementation("io.github.revxrsal:lamp.common:$lampVersion")
- implementation("io.github.revxrsal:lamp.velocity:$lampVersion")
- implementation("io.github.revxrsal:lamp.brigadier:$lampVersion")
+ implementation(libs.bstats.velocity)
+ implementation(libs.lamp.common)
+ implementation(libs.lamp.velocity)
+ implementation(libs.lamp.brigadier)
- compileOnly("com.github.retrooper:packetevents-velocity:$packetEventsVersion")
- compileOnly("com.velocitypowered:velocity-api:3.4.0")
+ compileOnly(libs.packetevents.velocity)
+ compileOnly(libs.velocity.api)
// Core implementation.
- implementation(project(":core"))
+ compileOnly(project(":core"))
}
tasks.processResources {
// Get versions.
- val packetEventsVersion: String by rootProject.extra
- val coreVersion: String by rootProject.extra
-
- // Replace plugin.yml
- filesMatching("velocity-plugin.json") {
- expand("version" to project.version)
- }
+ inputs.property("name", project.name)
+ inputs.property("version", project.version)
+ inputs.property("coreVersion", libs.versions.coreVersion.get())
+ inputs.property("packeteventsVersion", libs.versions.packetevents.get())
// Replace properties.
- filesMatching("platform.properties") {
+ filesMatching(listOf("velocity-plugin.json", "platform.properties")) {
+ expand(inputs.properties)
+ }
- expand(
- "name" to project.name,
- "version" to project.version,
- "coreVersion" to coreVersion,
- "packeteventsVersion" to packetEventsVersion
- )
+}
- }
+// A trick so netty used by async-http-client is always
+// relocated, but netty used by injector is always provided by platform (spigot, bungeecord, velocity)
+val coreProvider = provider { project(":core").tasks.shadowJar.flatMap { it.archiveFile } }
+tasks.jar {
+ enabled = false // only shadowJar is used
}
tasks.shadowJar {
// Wait for the core shadowJar to finish.
- mustRunAfter(project(":core").tasks.named("shadowJar"))
+ dependsOn(":core:shadowJar")
+
+ archiveBaseName = "CoralGate-Velocity"
+ archiveVersion = project.version.toString()
+ archiveClassifier = ""
- archiveBaseName.set("CoralGate-Velocity")
- archiveVersion.set(project.version.toString())
- archiveClassifier.set("")
+ from(zipTree(coreProvider)) // include shadowed core
// Relocate bStats.
relocate("org.bstats", "cloud.gteam.coralgate.libs.bstats")
@@ -91,6 +86,10 @@ tasks.shadowJar {
exclude("META-INF/*.DSA")
exclude("META-INF/*.RSA")
+ filesMatching("META-INF/*.kotlin_module") {
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+ }
+
}
tasks.build {
diff --git a/velocity/src/main/java/cloud/gteam/coralgate/VelocityPlugin.java b/velocity/src/main/java/cloud/gteam/coralgate/VelocityPlugin.java
index 3ac6128..5178aaa 100644
--- a/velocity/src/main/java/cloud/gteam/coralgate/VelocityPlugin.java
+++ b/velocity/src/main/java/cloud/gteam/coralgate/VelocityPlugin.java
@@ -21,6 +21,8 @@
import cloud.gteam.coralgate.commands.CoralGateCommand;
import cloud.gteam.coralgate.commands.VelocityPermissionChecker;
import cloud.gteam.coralgate.commands.permissions.PermissionFactory;
+import cloud.gteam.coralgate.injector.VelocityInjector;
+import cloud.gteam.coralgate.injector.handlers.VelocityNettyResponder;
import cloud.gteam.coralgate.processor.NetworkProcessor;
import cloud.gteam.coralgate.utils.ConfigUtils;
import cloud.gteam.coralgate.utils.PlatformUtils;
@@ -35,6 +37,7 @@
import org.bstats.velocity.Metrics;
import revxrsal.commands.Lamp;
import revxrsal.commands.velocity.VelocityLamp;
+import revxrsal.commands.velocity.VelocityVisitors;
import revxrsal.commands.velocity.actor.VelocityCommandActor;
import java.nio.file.Path;
@@ -42,31 +45,35 @@
public final class VelocityPlugin {
- private final Metrics.Factory metricsFactory;
-
+ private final CorePlugin corePlugin = new CorePlugin();
+ private final VelocityInjector injector;
private final Path dataDirectory;
+ private final Metrics.Factory metricsFactory;
@Inject
- public VelocityPlugin(final ProxyServer server, final @DataDirectory Path dataDirectory, final Metrics.Factory metricsFactory) {
+ public VelocityPlugin(final ProxyServer proxyServer, final @DataDirectory Path dataDirectory, final Metrics.Factory metricsFactory) {
this.dataDirectory = dataDirectory;
+ // Load bStats metrics factory.
+ this.metricsFactory = metricsFactory;
+
+ this.injector = new VelocityInjector(proxyServer);
// Load commands.
- final Lamp lamp = VelocityLamp.builder(this, server)
+ final Lamp lamp = VelocityLamp.builder(this, proxyServer)
.permissionFactory(new PermissionFactory(new VelocityPermissionChecker()))
.build();
lamp.register(new CoralGateCommand(this.corePlugin));
- // Load bStats metrics factory.
- this.metricsFactory = metricsFactory;
+ lamp.accept(VelocityVisitors.brigadier(proxyServer));
}
- private final CorePlugin corePlugin = new CorePlugin();
-
@Subscribe
public void onProxyInitialization(final ProxyInitializeEvent proxyInitializeEvent) {
+ this.injector.inject();
+
// Start bStats.
this.metricsFactory.make(this, 29439);
@@ -75,13 +82,15 @@ public void onProxyInitialization(final ProxyInitializeEvent proxyInitializeEven
new NetworkProcessor(getCorePlugin()), PacketListenerPriority.HIGHEST);
// Load core.
- this.corePlugin.onEnable(Logger.getLogger("CoralGate"), this.dataDirectory.toFile(), ConfigUtils.isOnlineMode("velocity.toml"), "velocity.toml", PlatformUtils.loadProperties(this.getClass()));
+ this.corePlugin.onEnable(Logger.getLogger("CoralGate"), this.dataDirectory.toFile(), ConfigUtils.isOnlineMode("velocity.toml"), "velocity.toml", PlatformUtils.loadProperties(this.getClass()), new VelocityNettyResponder());
}
@Subscribe
public void onProxyShutdown(final ProxyShutdownEvent proxyShutdownEvent) {
+ this.injector.uninject();
+
this.corePlugin.onDisable();
}
diff --git a/velocity/src/main/java/cloud/gteam/coralgate/injector/VelocityInjector.java b/velocity/src/main/java/cloud/gteam/coralgate/injector/VelocityInjector.java
new file mode 100644
index 0000000..552ab9b
--- /dev/null
+++ b/velocity/src/main/java/cloud/gteam/coralgate/injector/VelocityInjector.java
@@ -0,0 +1,137 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector;
+
+import cloud.gteam.coralgate.CorePlugin;
+import cloud.gteam.coralgate.injector.connection.VelocityChannelInitializer;
+import com.github.retrooper.packetevents.util.reflection.Reflection;
+import com.github.retrooper.packetevents.util.reflection.ReflectionObject;
+import com.velocitypowered.api.proxy.ProxyServer;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import org.jetbrains.annotations.ApiStatus;
+import org.spongepowered.configurate.util.CheckedConsumer;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.function.Supplier;
+
+@ApiStatus.Internal
+public class VelocityInjector {
+
+ public static final String DECODER_NAME = "cg-decoder";
+
+ private static Class> CONNECTION_MANAGER_CLASS, SERVER_INITIALIZER_HOLDER_CLASS;
+ private static Method SET_SERVER_INITIALIZER;
+
+ public boolean hasInjected = false;
+
+ private final ProxyServer server;
+
+ public VelocityInjector(final ProxyServer server) {
+ this.server = server;
+ }
+
+ public void inject() {
+
+ if (CONNECTION_MANAGER_CLASS == null) {
+
+ CONNECTION_MANAGER_CLASS = Reflection.getClassByNameWithoutException("com.velocitypowered.proxy.network.ConnectionManager");
+ SERVER_INITIALIZER_HOLDER_CLASS = Reflection.getClassByNameWithoutException("com.velocitypowered.proxy.network.ServerChannelInitializerHolder");
+ SET_SERVER_INITIALIZER = Reflection.getMethod(SERVER_INITIALIZER_HOLDER_CLASS, 0, ChannelInitializer.class);
+
+ }
+
+ final Supplier> initializerHolder = getServerChannelInitializerHolder();
+ final ChannelInitializer wrappedProxyInitializer = initializerHolder.get();
+ final VelocityChannelInitializer initializer = new VelocityChannelInitializer(wrappedProxyInitializer);
+ try {
+
+ SET_SERVER_INITIALIZER.invoke(initializerHolder, initializer);
+ hasInjected = true;
+
+ } catch (final IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void uninject() {
+
+ final Supplier> holder = this.getServerChannelInitializerHolder();
+ ChannelInitializer> wrapper = holder.get();
+ CheckedConsumer, ReflectiveOperationException> uninjector = (initializer) -> {
+
+ CorePlugin.getLogger().info("Uninjecting from Velocity channel initializer...");
+ SET_SERVER_INITIALIZER.invoke(holder, initializer);
+
+ };
+
+ try {
+
+ while (true) {
+
+ // Check if it's our initializer, could be wrapped by other plugins.
+ if (wrapper instanceof VelocityChannelInitializer wrappedInitializer) {
+
+ uninjector.accept(wrappedInitializer.getWrappedInitializer());
+ break;
+
+ } else {
+
+ // Walk up wrapper tree, if possible to find a single matching field.
+ // This accounts for other plugins (e.g. ViaVersion) also replacing the injector, which may
+ // wrap our already wrapped injector.
+ final Field field = Reflection.getField(wrapper.getClass(), ChannelInitializer.class, 0);
+ if (field == null) {
+ throw new IllegalStateException("Can't unwrap foreign channel initializer: " + wrapper);
+ }
+
+ field.setAccessible(true);
+
+ final ChannelInitializer> thisWrapper = wrapper;
+ wrapper = (ChannelInitializer>) field.get(thisWrapper);
+ uninjector = initializer -> {
+
+ field.set(thisWrapper, initializer);
+ CorePlugin.getLogger().info("Uninjected from plugin channel initializer " + thisWrapper);
+
+ };
+
+ }
+
+ }
+
+ } catch (final ReflectiveOperationException e) {
+ throw new RuntimeException("Failed to uninject from frontend pipeline", e);
+ }
+
+ }
+
+ private Supplier> getServerChannelInitializerHolder() {
+
+ final ReflectionObject reflectServer = new ReflectionObject(server);
+ final Object connectionManager = reflectServer.readObject(0, CONNECTION_MANAGER_CLASS);
+ final ReflectionObject reflectConnectionManager = new ReflectionObject(connectionManager);
+ return (Supplier>) reflectConnectionManager.readObject(0, SERVER_INITIALIZER_HOLDER_CLASS);
+
+ }
+
+}
diff --git a/velocity/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java b/velocity/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java
new file mode 100644
index 0000000..d2e2af1
--- /dev/null
+++ b/velocity/src/main/java/cloud/gteam/coralgate/injector/connection/ServerConnectionInitializer.java
@@ -0,0 +1,45 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import cloud.gteam.coralgate.injector.VelocityInjector;
+import cloud.gteam.coralgate.injector.handlers.VelocityDecoder;
+import com.github.retrooper.packetevents.protocol.ConnectionState;
+import com.github.retrooper.packetevents.protocol.player.User;
+import com.github.retrooper.packetevents.protocol.player.UserProfile;
+import io.netty.channel.Channel;
+
+public class ServerConnectionInitializer {
+
+ public static void addChannelHandlers(final Channel channel, final VelocityDecoder decoder) {
+ channel.pipeline().addBefore("legacy-ping-decoder", VelocityInjector.DECODER_NAME, decoder);
+ }
+
+ public static void initChannel(final Channel channel, final ConnectionState state) {
+
+ final VelocityDecoder decoder = new VelocityDecoder(new User(channel, state, null, new UserProfile(null, null)));
+ addChannelHandlers(channel, decoder);
+
+ }
+
+ public static void destroyChannel(final Channel channel) {
+ channel.pipeline().remove(VelocityInjector.DECODER_NAME);
+ }
+
+}
diff --git a/velocity/src/main/java/cloud/gteam/coralgate/injector/connection/VelocityChannelInitializer.java b/velocity/src/main/java/cloud/gteam/coralgate/injector/connection/VelocityChannelInitializer.java
new file mode 100644
index 0000000..6b5dc21
--- /dev/null
+++ b/velocity/src/main/java/cloud/gteam/coralgate/injector/connection/VelocityChannelInitializer.java
@@ -0,0 +1,62 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.connection;
+
+import com.github.retrooper.packetevents.PacketEvents;
+import com.github.retrooper.packetevents.protocol.ConnectionState;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Method;
+
+public class VelocityChannelInitializer extends ChannelInitializer {
+
+ private static Method INIT_CHANNEL;
+ private final ChannelInitializer wrappedInitializer;
+
+ public VelocityChannelInitializer(final ChannelInitializer wrappedInitializer) {
+ this.wrappedInitializer = wrappedInitializer;
+ }
+
+ @Override
+ protected void initChannel(final @NotNull Channel channel) throws Exception {
+
+ if (INIT_CHANNEL == null) {
+
+ INIT_CHANNEL = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
+ INIT_CHANNEL.setAccessible(true);
+
+ }
+
+ INIT_CHANNEL.invoke(this.wrappedInitializer, channel);
+
+ if (PacketEvents.getAPI().isTerminated()) {
+ return;
+ }
+
+ ServerConnectionInitializer.initChannel(channel, ConnectionState.HANDSHAKING);
+
+ }
+
+ public ChannelInitializer getWrappedInitializer() {
+ return this.wrappedInitializer;
+ }
+
+}
diff --git a/velocity/src/main/java/cloud/gteam/coralgate/injector/handlers/VelocityDecoder.java b/velocity/src/main/java/cloud/gteam/coralgate/injector/handlers/VelocityDecoder.java
new file mode 100644
index 0000000..99cd898
--- /dev/null
+++ b/velocity/src/main/java/cloud/gteam/coralgate/injector/handlers/VelocityDecoder.java
@@ -0,0 +1,125 @@
+/*
+ * This file is part of packetevents - https://github.com/retrooper/packetevents
+ * Copyright (C) 2022 retrooper 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 .
+ */
+
+package cloud.gteam.coralgate.injector.handlers;
+
+import cloud.gteam.coralgate.injector.connection.ServerConnectionInitializer;
+import com.github.retrooper.packetevents.PacketEvents;
+import com.github.retrooper.packetevents.event.simple.PacketHandshakeReceiveEvent;
+import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
+import com.github.retrooper.packetevents.protocol.player.User;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageDecoder;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class VelocityDecoder extends MessageToMessageDecoder {
+
+ public final User user;
+
+ public VelocityDecoder(final User user) {
+ this.user = user;
+ }
+
+ public void read(final ChannelHandlerContext ctx, final ByteBuf byteBuf, final List output) throws Exception {
+
+ final int firstReaderIndex = byteBuf.readerIndex();
+ final int readable = byteBuf.readableBytes();
+ boolean isLegacyServerListPing = false;
+
+ // Detect every legacy server list ping variant by peeking bytes directly (never consuming
+ // the reader index), rather than parsing a VarInt. A VarInt read would throw on the
+ // single-byte pre-1.4 variant since there's no second byte to complete it.
+ if (readable >= 1 && byteBuf.getUnsignedByte(firstReaderIndex) == 0xFE) {
+
+ // Bare single 0xFE, nothing trailing, pre-1.4 clients (1.1, 1.2, 1.3).
+ if (readable == 1) {
+
+ isLegacyServerListPing = true;
+
+ } else if (byteBuf.getUnsignedByte(firstReaderIndex + 1) == 0x01) {
+
+ if (readable == 2) {
+
+ // FE 01, nothing trailing, 1.4/1.5 clients.
+ isLegacyServerListPing = true;
+
+ } else if (readable >= 3 && byteBuf.getUnsignedByte(firstReaderIndex + 2) == 0xFA) {
+
+ // FE 01 FA ..., 1.6 clients.
+ isLegacyServerListPing = true;
+
+ }
+
+ }
+
+ }
+
+ if (!isLegacyServerListPing) {
+
+ output.add(byteBuf.retain());
+ return;
+
+ }
+
+ final PacketHandshakeReceiveEvent packetReceiveEvent = new PacketHandshakeReceiveEvent(ctx.channel(), this.user, null, byteBuf, false);
+ PacketEvents.getAPI().getEventManager().callEvent(packetReceiveEvent, () -> byteBuf.readerIndex(byteBuf.readerIndex()));
+
+ // No action is taken about the legacy packet here.
+ // NetworkProcessor receives the packet (like any other packet), and then decides what to do,
+ // including which legacy reply format to use (it re-derives that from the buffer itself).
+ // If the packet is canceled, it's cleared. Else we just pass it as any normal packet would.
+ if (packetReceiveEvent.isCancelled()) {
+ ByteBufHelper.clear(byteBuf);
+ } else {
+
+ byteBuf.readerIndex(firstReaderIndex);
+ output.add(byteBuf.retain());
+
+ }
+
+ if (packetReceiveEvent.hasPostTasks()) {
+
+ for (final Runnable task : packetReceiveEvent.getPostTasks()) {
+ task.run();
+ }
+
+ }
+
+ }
+
+ @Override
+ public void decode(final ChannelHandlerContext ctx, final ByteBuf buffer, final List out) throws Exception {
+
+ if (buffer.isReadable()) {
+ read(ctx, buffer, out);
+ }
+
+ }
+
+ @Override
+ public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
+
+ ServerConnectionInitializer.destroyChannel(ctx.channel());
+ super.channelInactive(ctx);
+
+ }
+
+}
diff --git a/velocity/src/main/java/cloud/gteam/coralgate/injector/handlers/VelocityNettyResponder.java b/velocity/src/main/java/cloud/gteam/coralgate/injector/handlers/VelocityNettyResponder.java
new file mode 100644
index 0000000..0d4ce07
--- /dev/null
+++ b/velocity/src/main/java/cloud/gteam/coralgate/injector/handlers/VelocityNettyResponder.java
@@ -0,0 +1,70 @@
+/*
+ * This file is part of CoralGate - https://github.com/GTeamX/CoralGate
+ * Copyright (C) 2026 GTeamX (GTeam) and it's 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 .
+ */
+
+package cloud.gteam.coralgate.injector.handlers;
+
+import cloud.gteam.coralgate.injector.NettyResponder;
+import com.github.retrooper.packetevents.protocol.player.User;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+
+import java.nio.charset.StandardCharsets;
+
+public class VelocityNettyResponder implements NettyResponder {
+
+ public VelocityNettyResponder() {}
+
+ @Override
+ public void sendLegacyPingResponse(final User user, final int protocolVersion, final String serverVersion, final String motd, final int onlinePlayers, final int maxPlayers) {
+
+ final String response = "§1\0"
+ + protocolVersion + "\0"
+ + serverVersion + "\0"
+ + motd + "\0"
+ + onlinePlayers + "\0"
+ + maxPlayers;
+ write(user, response);
+
+ }
+
+ @Override
+ public void sendOldLegacyPingResponse(final User user, final String motd, final int onlinePlayers, final int maxPlayers) {
+
+ final String response = motd + "§" + onlinePlayers + "§" + maxPlayers;
+ write(user, response);
+
+ }
+
+ private void write(final User user, final String response) {
+
+ final Channel channel = (Channel) user.getChannel();
+ final byte[] responseBytes = response.getBytes(StandardCharsets.UTF_16BE);
+
+ final ByteBuf buf = channel.alloc().buffer();
+ buf.writeByte(0xFF);
+ buf.writeShort(response.length());
+ buf.writeBytes(responseBytes);
+
+ channel.pipeline().firstContext()
+ .writeAndFlush(buf)
+ .addListener(ChannelFutureListener.CLOSE);
+
+ }
+
+}