diff --git a/backend-join-guard/README.md b/backend-join-guard/README.md
new file mode 100644
index 0000000..d638637
--- /dev/null
+++ b/backend-join-guard/README.md
@@ -0,0 +1,192 @@
+# BackendJoinGuard
+
+
+
+
+
+BackendJoinGuard blockiert direkte Joins auf Backend-Servern und erlaubt nur Verbindungen vom Proxy.
+
+Wichtig:
+- Dieses Plugin kommt auf jeden Backend-Server.
+- Es kommt nicht auf den Proxy.
+- Es kann komplett eigenständig genutzt werden.
+- Optional kann es seine Regeln zentral von StatusAPI beziehen.
+
+## Zweck
+
+BackendJoinGuard verhindert, dass Spieler einen Unterserver direkt betreten und damit den Proxy-Schutz umgehen.
+
+Das Plugin prüft beim Login die Quell-IP des Spielers.
+Nur erlaubte Proxy-IPs oder CIDR-Netze dürfen durchgelassen werden.
+Alle anderen Verbindungen werden direkt vor dem Join geblockt.
+
+## Betriebsarten
+
+### Standalone
+
+BackendJoinGuard nutzt nur seine eigene config.yml.
+Das ist sinnvoll, wenn du keinen zentralen Sync willst oder das Plugin unabhängig von StatusAPI betreiben möchtest.
+
+### StatusAPI Sync
+
+BackendJoinGuard lädt seine Schutzregeln regelmäßig von StatusAPI.
+Wenn der Sync ausfällt, bleiben die lokalen Werte aus der config.yml als Fallback erhalten.
+
+## Installation
+
+1. Die JAR in den plugins-Ordner jedes Backend-Servers kopieren.
+2. Server starten, damit die config.yml erzeugt wird.
+3. Konfiguration anpassen.
+4. Server neu starten oder /backendguard reload ausführen.
+
+## Konfiguration
+
+Datei: config.yml
+
+### Lokale Guard-Werte
+
+- enforcement-enabled
+ Aktiviert oder deaktiviert den Direktjoin-Schutz.
+
+- log-blocked-attempts
+ Loggt geblockte Direktjoin-Versuche in die Konsole.
+
+- kick-message
+ Nachricht für geblockte Spieler.
+
+- allowed-proxy-ips
+ Liste exakter Proxy-IPs.
+
+- allowed-proxy-cidrs
+ Liste erlaubter Netze im CIDR-Format.
+
+Beispiel:
+
+```yml
+enforcement-enabled: true
+log-blocked-attempts: true
+kick-message: "&cBitte verbinde dich nur ueber den Proxy-Server."
+
+allowed-proxy-ips:
+ - "127.0.0.1"
+ - "::1"
+ - "185.123.45.67"
+
+allowed-proxy-cidrs:
+ - "10.0.0.0/24"
+```
+
+### StatusAPI Sync
+
+Konfigurationsblock:
+
+```yml
+statusapi-sync:
+ enabled: true
+ base-url: "http://127.0.0.1:9191"
+ endpoint-path: "/network/backendguard/config"
+ api-key: "DEIN_SYNC_KEY"
+ interval-seconds: 60
+ log-sync-errors: true
+```
+
+Bedeutung:
+
+- enabled
+ Aktiviert den zentralen Abruf über StatusAPI.
+
+- base-url
+ Basis-URL deiner StatusAPI auf dem Proxy.
+
+- endpoint-path
+ Standard-Endpunkt für den Guard-Sync.
+
+- api-key
+ Optionaler Schutz für den Sync-Endpunkt.
+ Muss identisch zu backendguard.sync.api_key in StatusAPI sein.
+
+- interval-seconds
+ Wie oft BackendJoinGuard die Regeln neu abruft.
+
+- log-sync-errors
+ Loggt Sync-Probleme in die Backend-Konsole.
+
+## Passende StatusAPI-Konfiguration
+
+Wenn du den Sync-Modus nutzen willst, pflegst du die zentralen Werte in StatusAPI über network-guard.properties.
+
+Relevante Schlüssel:
+
+```properties
+backendguard.enforcement_enabled=true
+backendguard.log_blocked_attempts=true
+backendguard.kick_message=&cBitte verbinde dich nur ueber den Proxy-Server.
+backendguard.allowed_proxy_ips=127.0.0.1,::1,185.123.45.67
+backendguard.allowed_proxy_cidrs=10.0.0.0/24
+backendguard.sync.api_key=DEIN_SYNC_KEY
+```
+
+## Beispiele
+
+### Alles auf einer Maschine
+
+Nutze in der Regel:
+
+- allowed-proxy-ips: 127.0.0.1, ::1
+- base-url: http://127.0.0.1:9191
+
+### Proxy auf einem anderen Host
+
+Nutze die echte Proxy-IP:
+
+- allowed-proxy-ips: 185.123.45.67
+- base-url: http://185.123.45.67:9191
+
+### Internes Netzwerk
+
+Nutze interne IP oder CIDR:
+
+- allowed-proxy-ips: 10.0.0.10
+- allowed-proxy-cidrs: 10.0.0.0/24
+- base-url: http://10.0.0.10:9191
+
+## Befehl
+
+| Command | Beschreibung |
+|---|---|
+| /backendguard reload | Lädt die Config neu und startet bei aktivem Sync sofort einen neuen Abruf |
+
+## Permission
+
+| Permission | Beschreibung |
+|---|---|
+| backendguard.admin | Darf BackendJoinGuard verwalten |
+
+## Funktionsweise
+
+BackendJoinGuard prüft beim PreLogin die IP-Adresse des Clients.
+
+- Erlaubte Proxy-IP: Join erlaubt
+- Erlaubtes CIDR-Netz: Join erlaubt
+- Alles andere: Join wird direkt geblockt
+
+## Troubleshooting
+
+### Spieler werden trotz Proxy geblockt
+
+- Prüfe, ob wirklich die Proxy-IP in allowed-proxy-ips eingetragen ist.
+- Prüfe bei mehreren Netzsegmenten, ob ein CIDR-Eintrag sinnvoller ist.
+- Prüfe, ob Velocity/Bungee korrekt als vorgeschalteter Proxy arbeitet.
+
+### Sync mit StatusAPI funktioniert nicht
+
+- Prüfe base-url.
+- Prüfe, ob StatusAPI auf dem konfigurierten Port erreichbar ist.
+- Prüfe, ob api-key identisch zu backendguard.sync.api_key ist.
+- Prüfe die Backend-Konsole bei aktiviertem log-sync-errors.
+
+### Standalone oder Sync?
+
+- Wenn du maximale Einfachheit willst: Standalone.
+- Wenn du zentrale Verwaltung willst: StatusAPI Sync.
+- Wenn StatusAPI ausfällt, bleiben im Sync-Modus die lokalen Werte als Absicherung bestehen.
\ No newline at end of file
diff --git a/backend-join-guard/pom.xml b/backend-join-guard/pom.xml
index e4b3b30..e340f8d 100644
--- a/backend-join-guard/pom.xml
+++ b/backend-join-guard/pom.xml
@@ -1,46 +1,46 @@
-
-
- 4.0.0
-
- net.viper.backend
- backend-join-guard
- 1.0.0
- jar
-
- BackendJoinGuard
- Blocks direct joins to backend servers and only allows proxy IPs.
-
-
- 17
- 17
- UTF-8
-
-
-
-
- papermc
- https://repo.papermc.io/repository/maven-public/
-
-
-
-
-
- io.papermc.paper
- paper-api
- 1.20.6-R0.1-SNAPSHOT
- provided
-
-
-
-
- BackendJoinGuard
-
-
- src/main/resources
- false
-
-
-
-
+
+
+ 4.0.0
+
+ net.viper.backend
+ backend-join-guard
+ 1.0.0
+ jar
+
+ BackendJoinGuard
+ Blocks direct joins to backend servers and only allows proxy IPs.
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+
+
+
+
+ io.papermc.paper
+ paper-api
+ 1.20.6-R0.1-SNAPSHOT
+ provided
+
+
+
+
+ BackendJoinGuard
+
+
+ src/main/resources
+ false
+
+
+
+
diff --git a/backend-join-guard/src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java b/backend-join-guard/src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java
index f4d9c3f..b0737fd 100644
--- a/backend-join-guard/src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java
+++ b/backend-join-guard/src/main/java/net/viper/backendguard/BackendJoinGuardPlugin.java
@@ -1,509 +1,515 @@
-package net.viper.backendguard;
-
-import org.bukkit.ChatColor;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
-import org.bukkit.plugin.java.JavaPlugin;
-import org.bukkit.scheduler.BukkitTask;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-
-public class BackendJoinGuardPlugin extends JavaPlugin implements Listener {
-
- private boolean enforcementEnabled;
- private boolean logBlockedAttempts;
- private String kickMessage;
- private final Set allowedExactIps = new HashSet();
- private final List allowedCidrs = new ArrayList();
- private boolean statusApiSyncEnabled;
- private String statusApiBaseUrl;
- private String statusApiEndpointPath;
- private String statusApiApiKey;
- private int statusApiIntervalSeconds;
- private boolean statusApiLogSyncErrors;
- private BukkitTask syncTask;
-
- @Override
- public void onEnable() {
- saveDefaultConfig();
- reloadGuardConfig();
- getServer().getPluginManager().registerEvents(this, this);
- getLogger().info("BackendJoinGuard aktiviert. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size() + ", StatusAPI-Sync=" + statusApiSyncEnabled);
- }
-
- @Override
- public void onDisable() {
- cancelSyncTask();
- }
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
- if (!enforcementEnabled) {
- return;
- }
-
- InetAddress address = event.getAddress();
- if (address == null) {
- return;
- }
-
- if (isAllowed(address)) {
- return;
- }
-
- event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, colorize(kickMessage));
- if (logBlockedAttempts) {
- getLogger().warning("Direktjoin blockiert: player=" + event.getName() + ", ip=" + address.getHostAddress());
- }
- }
-
- @Override
- public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- if (!"backendguard".equalsIgnoreCase(command.getName())) {
- return false;
- }
-
- if (!sender.hasPermission("backendguard.admin")) {
- sender.sendMessage(colorize("&cDafuer hast du keine Rechte."));
- return true;
- }
-
- if (args.length == 1 && "reload".equalsIgnoreCase(args[0])) {
- reloadConfig();
- reloadGuardConfig();
- if (statusApiSyncEnabled) {
- syncFromStatusApi(true);
- sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. StatusAPI-Sync aktiv."));
- } else {
- sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. Standalone-Modus aktiv."));
- }
- return true;
- }
-
- sender.sendMessage(colorize("&e/backendguard reload"));
- return true;
- }
-
- private void reloadGuardConfig() {
- FileConfiguration config = getConfig();
- enforcementEnabled = config.getBoolean("enforcement-enabled", true);
- logBlockedAttempts = config.getBoolean("log-blocked-attempts", true);
- kickMessage = config.getString("kick-message", "&cBitte verbinde dich nur ueber den Proxy.");
-
- allowedExactIps.clear();
- allowedCidrs.clear();
-
- for (String entry : config.getStringList("allowed-proxy-ips")) {
- String normalized = normalizeIp(entry);
- if (normalized != null) {
- allowedExactIps.add(normalized);
- }
- }
-
- for (String entry : config.getStringList("allowed-proxy-cidrs")) {
- SubnetRule rule = parseSubnet(entry);
- if (rule != null) {
- allowedCidrs.add(rule);
- }
- }
-
- statusApiSyncEnabled = config.getBoolean("statusapi-sync.enabled", false);
- statusApiBaseUrl = config.getString("statusapi-sync.base-url", "http://127.0.0.1:9191");
- statusApiEndpointPath = config.getString("statusapi-sync.endpoint-path", "/network/backendguard/config");
- statusApiApiKey = config.getString("statusapi-sync.api-key", "");
- statusApiIntervalSeconds = Math.max(10, config.getInt("statusapi-sync.interval-seconds", 60));
- statusApiLogSyncErrors = config.getBoolean("statusapi-sync.log-sync-errors", true);
-
- cancelSyncTask();
- if (statusApiSyncEnabled) {
- // Erst lokale Werte laden, dann zentral ueberschreiben falls Sync klappt.
- syncFromStatusApi(false);
- syncTask = getServer().getScheduler().runTaskTimerAsynchronously(
- this,
- () -> syncFromStatusApi(false),
- statusApiIntervalSeconds * 20L,
- statusApiIntervalSeconds * 20L
- );
- }
- }
-
- private void cancelSyncTask() {
- if (syncTask != null) {
- syncTask.cancel();
- syncTask = null;
- }
- }
-
- private void syncFromStatusApi(boolean manualTrigger) {
- if (!statusApiSyncEnabled) {
- return;
- }
-
- HttpURLConnection conn = null;
- try {
- URL url = new URL(buildSyncUrl());
- conn = (HttpURLConnection) url.openConnection();
- conn.setRequestMethod("GET");
- conn.setConnectTimeout(4000);
- conn.setReadTimeout(6000);
- conn.setRequestProperty("Accept", "application/json");
- if (statusApiApiKey != null && !statusApiApiKey.trim().isEmpty()) {
- conn.setRequestProperty("x-api-key", statusApiApiKey.trim());
- }
-
- int code = conn.getResponseCode();
- if (code < 200 || code >= 300) {
- if (statusApiLogSyncErrors || manualTrigger) {
- getLogger().warning("StatusAPI-Sync fehlgeschlagen (HTTP " + code + "). Nutze lokale Guard-Werte.");
- }
- return;
- }
-
- StringBuilder sb = new StringBuilder();
- try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
- String line;
- while ((line = br.readLine()) != null) {
- sb.append(line);
- }
- }
-
- String json = sb.toString();
- Boolean remoteEnforcement = extractJsonBoolean(json, "enforcement_enabled");
- Boolean remoteLogBlocked = extractJsonBoolean(json, "log_blocked_attempts");
- String remoteKickMessage = extractJsonString(json, "kick_message");
- List remoteIps = extractJsonStringArray(json, "allowed_proxy_ips");
- List remoteCidrs = extractJsonStringArray(json, "allowed_proxy_cidrs");
-
- if (remoteEnforcement != null) {
- enforcementEnabled = remoteEnforcement;
- }
- if (remoteLogBlocked != null) {
- logBlockedAttempts = remoteLogBlocked;
- }
- if (remoteKickMessage != null && !remoteKickMessage.trim().isEmpty()) {
- kickMessage = remoteKickMessage;
- }
-
- if (remoteIps != null) {
- allowedExactIps.clear();
- for (String ip : remoteIps) {
- String normalized = normalizeIp(ip);
- if (normalized != null) {
- allowedExactIps.add(normalized);
- }
- }
- }
-
- if (remoteCidrs != null) {
- allowedCidrs.clear();
- for (String cidr : remoteCidrs) {
- SubnetRule rule = parseSubnet(cidr);
- if (rule != null) {
- allowedCidrs.add(rule);
- }
- }
- }
-
- if (manualTrigger) {
- getLogger().info("StatusAPI-Sync erfolgreich. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size());
- }
- } catch (Exception e) {
- if (statusApiLogSyncErrors || manualTrigger) {
- getLogger().warning("StatusAPI-Sync Fehler: " + e.getMessage() + ". Nutze lokale Guard-Werte.");
- }
- } finally {
- if (conn != null) {
- conn.disconnect();
- }
- }
- }
-
- private String buildSyncUrl() {
- String base = statusApiBaseUrl == null ? "" : statusApiBaseUrl.trim();
- String path = statusApiEndpointPath == null ? "" : statusApiEndpointPath.trim();
-
- if (base.endsWith("/") && path.startsWith("/")) {
- return base.substring(0, base.length() - 1) + path;
- }
- if (!base.endsWith("/") && !path.startsWith("/")) {
- return base + "/" + path;
- }
- return base + path;
- }
-
- private Boolean extractJsonBoolean(String json, String key) {
- String token = extractJsonToken(json, key);
- if (token == null) {
- return null;
- }
- if ("true".equalsIgnoreCase(token)) {
- return Boolean.TRUE;
- }
- if ("false".equalsIgnoreCase(token)) {
- return Boolean.FALSE;
- }
- return null;
- }
-
- private String extractJsonString(String json, String key) {
- if (json == null || key == null) {
- return null;
- }
- String search = "\"" + key + "\"";
- int idx = json.indexOf(search);
- if (idx < 0) {
- return null;
- }
- int colon = json.indexOf(':', idx + search.length());
- if (colon < 0) {
- return null;
- }
- int i = colon + 1;
- while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
- i++;
- }
- if (i >= json.length() || json.charAt(i) != '"') {
- return null;
- }
-
- i++;
- StringBuilder sb = new StringBuilder();
- boolean escape = false;
- while (i < json.length()) {
- char ch = json.charAt(i++);
- if (escape) {
- if (ch == 'n') {
- sb.append('\n');
- } else if (ch == 'r') {
- sb.append('\r');
- } else {
- sb.append(ch);
- }
- escape = false;
- continue;
- }
- if (ch == '\\') {
- escape = true;
- continue;
- }
- if (ch == '"') {
- break;
- }
- sb.append(ch);
- }
- return sb.toString();
- }
-
- private String extractJsonToken(String json, String key) {
- if (json == null || key == null) {
- return null;
- }
- String search = "\"" + key + "\"";
- int idx = json.indexOf(search);
- if (idx < 0) {
- return null;
- }
- int colon = json.indexOf(':', idx + search.length());
- if (colon < 0) {
- return null;
- }
- int i = colon + 1;
- while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
- i++;
- }
- if (i >= json.length()) {
- return null;
- }
-
- StringBuilder sb = new StringBuilder();
- while (i < json.length()) {
- char ch = json.charAt(i);
- if (ch == ',' || ch == '}' || ch == ']' || Character.isWhitespace(ch)) {
- break;
- }
- sb.append(ch);
- i++;
- }
- return sb.toString().trim();
- }
-
- private List extractJsonStringArray(String json, String key) {
- if (json == null || key == null) {
- return null;
- }
-
- String search = "\"" + key + "\"";
- int idx = json.indexOf(search);
- if (idx < 0) {
- return null;
- }
-
- int colon = json.indexOf(':', idx + search.length());
- if (colon < 0) {
- return null;
- }
-
- int start = json.indexOf('[', colon + 1);
- if (start < 0) {
- return null;
- }
-
- int depth = 0;
- int end = -1;
- for (int i = start; i < json.length(); i++) {
- char ch = json.charAt(i);
- if (ch == '[') {
- depth++;
- } else if (ch == ']') {
- depth--;
- if (depth == 0) {
- end = i;
- break;
- }
- }
- }
-
- if (end < 0 || end <= start) {
- return null;
- }
-
- String raw = json.substring(start + 1, end);
- List out = new ArrayList();
- boolean inString = false;
- boolean escape = false;
- StringBuilder current = new StringBuilder();
-
- for (int i = 0; i < raw.length(); i++) {
- char ch = raw.charAt(i);
- if (!inString) {
- if (ch == '"') {
- inString = true;
- current.setLength(0);
- }
- continue;
- }
-
- if (escape) {
- if (ch == 'n') {
- current.append('\n');
- } else if (ch == 'r') {
- current.append('\r');
- } else {
- current.append(ch);
- }
- escape = false;
- continue;
- }
-
- if (ch == '\\') {
- escape = true;
- continue;
- }
-
- if (ch == '"') {
- inString = false;
- out.add(current.toString());
- continue;
- }
-
- current.append(ch);
- }
-
- return out;
- }
-
- private boolean isAllowed(InetAddress address) {
- String normalized = normalizeIp(address.getHostAddress());
- if (normalized != null && allowedExactIps.contains(normalized)) {
- return true;
- }
-
- for (SubnetRule rule : allowedCidrs) {
- if (rule.matches(address)) {
- return true;
- }
- }
- return false;
- }
-
- private String normalizeIp(String raw) {
- if (raw == null || raw.trim().isEmpty()) {
- return null;
- }
- try {
- return InetAddress.getByName(raw.trim()).getHostAddress().toLowerCase(Locale.ROOT);
- } catch (UnknownHostException e) {
- getLogger().warning("Ungueltige IP in Config ignoriert: " + raw);
- return null;
- }
- }
-
- private SubnetRule parseSubnet(String raw) {
- if (raw == null || raw.trim().isEmpty() || !raw.contains("/")) {
- return null;
- }
-
- String[] parts = raw.trim().split("/", 2);
- try {
- InetAddress network = InetAddress.getByName(parts[0]);
- int prefix = Integer.parseInt(parts[1]);
- return new SubnetRule(network, prefix);
- } catch (Exception e) {
- getLogger().warning("Ungueltige CIDR in Config ignoriert: " + raw);
- return null;
- }
- }
-
- private String colorize(String text) {
- return ChatColor.translateAlternateColorCodes('&', text == null ? "" : text);
- }
-
- private static final class SubnetRule {
- private final byte[] networkBytes;
- private final int prefixLength;
-
- private SubnetRule(InetAddress network, int prefixLength) {
- this.networkBytes = network.getAddress();
- this.prefixLength = prefixLength;
- }
-
- private boolean matches(InetAddress address) {
- byte[] candidate = address.getAddress();
- if (candidate.length != networkBytes.length) {
- return false;
- }
-
- int fullBytes = prefixLength / 8;
- int remainderBits = prefixLength % 8;
-
- for (int i = 0; i < fullBytes; i++) {
- if (candidate[i] != networkBytes[i]) {
- return false;
- }
- }
-
- if (remainderBits == 0) {
- return true;
- }
-
- int mask = (-1) << (8 - remainderBits);
- return (candidate[fullBytes] & mask) == (networkBytes[fullBytes] & mask);
- }
- }
-}
+package net.viper.backendguard;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class BackendJoinGuardPlugin extends JavaPlugin implements Listener {
+
+ private boolean enforcementEnabled;
+ private boolean logBlockedAttempts;
+ private String kickMessage;
+ private final Set allowedExactIps = new HashSet();
+ private final List allowedCidrs = new ArrayList();
+ private boolean statusApiSyncEnabled;
+ private String statusApiBaseUrl;
+ private String statusApiEndpointPath;
+ private String statusApiApiKey;
+ private int statusApiIntervalSeconds;
+ private boolean statusApiLogSyncErrors;
+ private BukkitTask syncTask;
+
+ @Override
+ public void onEnable() {
+ saveDefaultConfig();
+ reloadGuardConfig();
+ getServer().getPluginManager().registerEvents(this, this);
+ getLogger().info("BackendJoinGuard aktiviert. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size() + ", StatusAPI-Sync=" + statusApiSyncEnabled);
+ }
+
+ @Override
+ public void onDisable() {
+ cancelSyncTask();
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
+ if (!enforcementEnabled) {
+ return;
+ }
+
+ // WICHTIG: getRawAddress() liefert die echte Verbindungsadresse (= der Proxy).
+ // event.getAddress() liefert bei aktivem BungeeCord-IP-Forwarding (spigot.yml: bungeecord: true)
+ // die IP des SPIELERS - die steht nie in der Proxy-Liste, daher wurden alle Spieler gekickt.
+ InetAddress address = event.getRawAddress();
+ if (address == null) {
+ address = event.getAddress();
+ }
+ if (address == null) {
+ return;
+ }
+
+ if (isAllowed(address)) {
+ return;
+ }
+
+ event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, colorize(kickMessage));
+ if (logBlockedAttempts) {
+ getLogger().warning("Direktjoin blockiert: player=" + event.getName() + ", ip=" + address.getHostAddress());
+ }
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (!"backendguard".equalsIgnoreCase(command.getName())) {
+ return false;
+ }
+
+ if (!sender.hasPermission("backendguard.admin")) {
+ sender.sendMessage(colorize("&cDafuer hast du keine Rechte."));
+ return true;
+ }
+
+ if (args.length == 1 && "reload".equalsIgnoreCase(args[0])) {
+ reloadConfig();
+ reloadGuardConfig();
+ if (statusApiSyncEnabled) {
+ syncFromStatusApi(true);
+ sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. StatusAPI-Sync aktiv."));
+ } else {
+ sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. Standalone-Modus aktiv."));
+ }
+ return true;
+ }
+
+ sender.sendMessage(colorize("&e/backendguard reload"));
+ return true;
+ }
+
+ private void reloadGuardConfig() {
+ FileConfiguration config = getConfig();
+ enforcementEnabled = config.getBoolean("enforcement-enabled", true);
+ logBlockedAttempts = config.getBoolean("log-blocked-attempts", true);
+ kickMessage = config.getString("kick-message", "&cBitte verbinde dich nur ueber den Proxy.");
+
+ allowedExactIps.clear();
+ allowedCidrs.clear();
+
+ for (String entry : config.getStringList("allowed-proxy-ips")) {
+ String normalized = normalizeIp(entry);
+ if (normalized != null) {
+ allowedExactIps.add(normalized);
+ }
+ }
+
+ for (String entry : config.getStringList("allowed-proxy-cidrs")) {
+ SubnetRule rule = parseSubnet(entry);
+ if (rule != null) {
+ allowedCidrs.add(rule);
+ }
+ }
+
+ statusApiSyncEnabled = config.getBoolean("statusapi-sync.enabled", false);
+ statusApiBaseUrl = config.getString("statusapi-sync.base-url", "http://127.0.0.1:9191");
+ statusApiEndpointPath = config.getString("statusapi-sync.endpoint-path", "/network/backendguard/config");
+ statusApiApiKey = config.getString("statusapi-sync.api-key", "");
+ statusApiIntervalSeconds = Math.max(10, config.getInt("statusapi-sync.interval-seconds", 60));
+ statusApiLogSyncErrors = config.getBoolean("statusapi-sync.log-sync-errors", true);
+
+ cancelSyncTask();
+ if (statusApiSyncEnabled) {
+ // Erst lokale Werte laden, dann zentral ueberschreiben falls Sync klappt.
+ syncFromStatusApi(false);
+ syncTask = getServer().getScheduler().runTaskTimerAsynchronously(
+ this,
+ () -> syncFromStatusApi(false),
+ statusApiIntervalSeconds * 20L,
+ statusApiIntervalSeconds * 20L
+ );
+ }
+ }
+
+ private void cancelSyncTask() {
+ if (syncTask != null) {
+ syncTask.cancel();
+ syncTask = null;
+ }
+ }
+
+ private void syncFromStatusApi(boolean manualTrigger) {
+ if (!statusApiSyncEnabled) {
+ return;
+ }
+
+ HttpURLConnection conn = null;
+ try {
+ URL url = new URL(buildSyncUrl());
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(4000);
+ conn.setReadTimeout(6000);
+ conn.setRequestProperty("Accept", "application/json");
+ if (statusApiApiKey != null && !statusApiApiKey.trim().isEmpty()) {
+ conn.setRequestProperty("x-api-key", statusApiApiKey.trim());
+ }
+
+ int code = conn.getResponseCode();
+ if (code < 200 || code >= 300) {
+ if (statusApiLogSyncErrors || manualTrigger) {
+ getLogger().warning("StatusAPI-Sync fehlgeschlagen (HTTP " + code + "). Nutze lokale Guard-Werte.");
+ }
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ }
+ }
+
+ String json = sb.toString();
+ Boolean remoteEnforcement = extractJsonBoolean(json, "enforcement_enabled");
+ Boolean remoteLogBlocked = extractJsonBoolean(json, "log_blocked_attempts");
+ String remoteKickMessage = extractJsonString(json, "kick_message");
+ List remoteIps = extractJsonStringArray(json, "allowed_proxy_ips");
+ List remoteCidrs = extractJsonStringArray(json, "allowed_proxy_cidrs");
+
+ if (remoteEnforcement != null) {
+ enforcementEnabled = remoteEnforcement;
+ }
+ if (remoteLogBlocked != null) {
+ logBlockedAttempts = remoteLogBlocked;
+ }
+ if (remoteKickMessage != null && !remoteKickMessage.trim().isEmpty()) {
+ kickMessage = remoteKickMessage;
+ }
+
+ if (remoteIps != null) {
+ allowedExactIps.clear();
+ for (String ip : remoteIps) {
+ String normalized = normalizeIp(ip);
+ if (normalized != null) {
+ allowedExactIps.add(normalized);
+ }
+ }
+ }
+
+ if (remoteCidrs != null) {
+ allowedCidrs.clear();
+ for (String cidr : remoteCidrs) {
+ SubnetRule rule = parseSubnet(cidr);
+ if (rule != null) {
+ allowedCidrs.add(rule);
+ }
+ }
+ }
+
+ if (manualTrigger) {
+ getLogger().info("StatusAPI-Sync erfolgreich. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size());
+ }
+ } catch (Exception e) {
+ if (statusApiLogSyncErrors || manualTrigger) {
+ getLogger().warning("StatusAPI-Sync Fehler: " + e.getMessage() + ". Nutze lokale Guard-Werte.");
+ }
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ private String buildSyncUrl() {
+ String base = statusApiBaseUrl == null ? "" : statusApiBaseUrl.trim();
+ String path = statusApiEndpointPath == null ? "" : statusApiEndpointPath.trim();
+
+ if (base.endsWith("/") && path.startsWith("/")) {
+ return base.substring(0, base.length() - 1) + path;
+ }
+ if (!base.endsWith("/") && !path.startsWith("/")) {
+ return base + "/" + path;
+ }
+ return base + path;
+ }
+
+ private Boolean extractJsonBoolean(String json, String key) {
+ String token = extractJsonToken(json, key);
+ if (token == null) {
+ return null;
+ }
+ if ("true".equalsIgnoreCase(token)) {
+ return Boolean.TRUE;
+ }
+ if ("false".equalsIgnoreCase(token)) {
+ return Boolean.FALSE;
+ }
+ return null;
+ }
+
+ private String extractJsonString(String json, String key) {
+ if (json == null || key == null) {
+ return null;
+ }
+ String search = "\"" + key + "\"";
+ int idx = json.indexOf(search);
+ if (idx < 0) {
+ return null;
+ }
+ int colon = json.indexOf(':', idx + search.length());
+ if (colon < 0) {
+ return null;
+ }
+ int i = colon + 1;
+ while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
+ i++;
+ }
+ if (i >= json.length() || json.charAt(i) != '"') {
+ return null;
+ }
+
+ i++;
+ StringBuilder sb = new StringBuilder();
+ boolean escape = false;
+ while (i < json.length()) {
+ char ch = json.charAt(i++);
+ if (escape) {
+ if (ch == 'n') {
+ sb.append('\n');
+ } else if (ch == 'r') {
+ sb.append('\r');
+ } else {
+ sb.append(ch);
+ }
+ escape = false;
+ continue;
+ }
+ if (ch == '\\') {
+ escape = true;
+ continue;
+ }
+ if (ch == '"') {
+ break;
+ }
+ sb.append(ch);
+ }
+ return sb.toString();
+ }
+
+ private String extractJsonToken(String json, String key) {
+ if (json == null || key == null) {
+ return null;
+ }
+ String search = "\"" + key + "\"";
+ int idx = json.indexOf(search);
+ if (idx < 0) {
+ return null;
+ }
+ int colon = json.indexOf(':', idx + search.length());
+ if (colon < 0) {
+ return null;
+ }
+ int i = colon + 1;
+ while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
+ i++;
+ }
+ if (i >= json.length()) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ while (i < json.length()) {
+ char ch = json.charAt(i);
+ if (ch == ',' || ch == '}' || ch == ']' || Character.isWhitespace(ch)) {
+ break;
+ }
+ sb.append(ch);
+ i++;
+ }
+ return sb.toString().trim();
+ }
+
+ private List extractJsonStringArray(String json, String key) {
+ if (json == null || key == null) {
+ return null;
+ }
+
+ String search = "\"" + key + "\"";
+ int idx = json.indexOf(search);
+ if (idx < 0) {
+ return null;
+ }
+
+ int colon = json.indexOf(':', idx + search.length());
+ if (colon < 0) {
+ return null;
+ }
+
+ int start = json.indexOf('[', colon + 1);
+ if (start < 0) {
+ return null;
+ }
+
+ int depth = 0;
+ int end = -1;
+ for (int i = start; i < json.length(); i++) {
+ char ch = json.charAt(i);
+ if (ch == '[') {
+ depth++;
+ } else if (ch == ']') {
+ depth--;
+ if (depth == 0) {
+ end = i;
+ break;
+ }
+ }
+ }
+
+ if (end < 0 || end <= start) {
+ return null;
+ }
+
+ String raw = json.substring(start + 1, end);
+ List out = new ArrayList();
+ boolean inString = false;
+ boolean escape = false;
+ StringBuilder current = new StringBuilder();
+
+ for (int i = 0; i < raw.length(); i++) {
+ char ch = raw.charAt(i);
+ if (!inString) {
+ if (ch == '"') {
+ inString = true;
+ current.setLength(0);
+ }
+ continue;
+ }
+
+ if (escape) {
+ if (ch == 'n') {
+ current.append('\n');
+ } else if (ch == 'r') {
+ current.append('\r');
+ } else {
+ current.append(ch);
+ }
+ escape = false;
+ continue;
+ }
+
+ if (ch == '\\') {
+ escape = true;
+ continue;
+ }
+
+ if (ch == '"') {
+ inString = false;
+ out.add(current.toString());
+ continue;
+ }
+
+ current.append(ch);
+ }
+
+ return out;
+ }
+
+ private boolean isAllowed(InetAddress address) {
+ String normalized = normalizeIp(address.getHostAddress());
+ if (normalized != null && allowedExactIps.contains(normalized)) {
+ return true;
+ }
+
+ for (SubnetRule rule : allowedCidrs) {
+ if (rule.matches(address)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String normalizeIp(String raw) {
+ if (raw == null || raw.trim().isEmpty()) {
+ return null;
+ }
+ try {
+ return InetAddress.getByName(raw.trim()).getHostAddress().toLowerCase(Locale.ROOT);
+ } catch (UnknownHostException e) {
+ getLogger().warning("Ungueltige IP in Config ignoriert: " + raw);
+ return null;
+ }
+ }
+
+ private SubnetRule parseSubnet(String raw) {
+ if (raw == null || raw.trim().isEmpty() || !raw.contains("/")) {
+ return null;
+ }
+
+ String[] parts = raw.trim().split("/", 2);
+ try {
+ InetAddress network = InetAddress.getByName(parts[0]);
+ int prefix = Integer.parseInt(parts[1]);
+ return new SubnetRule(network, prefix);
+ } catch (Exception e) {
+ getLogger().warning("Ungueltige CIDR in Config ignoriert: " + raw);
+ return null;
+ }
+ }
+
+ private String colorize(String text) {
+ return ChatColor.translateAlternateColorCodes('&', text == null ? "" : text);
+ }
+
+ private static final class SubnetRule {
+ private final byte[] networkBytes;
+ private final int prefixLength;
+
+ private SubnetRule(InetAddress network, int prefixLength) {
+ this.networkBytes = network.getAddress();
+ this.prefixLength = prefixLength;
+ }
+
+ private boolean matches(InetAddress address) {
+ byte[] candidate = address.getAddress();
+ if (candidate.length != networkBytes.length) {
+ return false;
+ }
+
+ int fullBytes = prefixLength / 8;
+ int remainderBits = prefixLength % 8;
+
+ for (int i = 0; i < fullBytes; i++) {
+ if (candidate[i] != networkBytes[i]) {
+ return false;
+ }
+ }
+
+ if (remainderBits == 0) {
+ return true;
+ }
+
+ int mask = (-1) << (8 - remainderBits);
+ return (candidate[fullBytes] & mask) == (networkBytes[fullBytes] & mask);
+ }
+ }
+}
diff --git a/backend-join-guard/src/main/resources/config.yml b/backend-join-guard/src/main/resources/config.yml
index 076d370..f27584d 100644
--- a/backend-join-guard/src/main/resources/config.yml
+++ b/backend-join-guard/src/main/resources/config.yml
@@ -1,29 +1,29 @@
-# BackendJoinGuard
-# Dieses Plugin kommt auf JEDEN Unterserver (Paper/Spigot), nicht auf den Proxy.
-# Nur Verbindungen von diesen Proxy-IPs oder Netzbereichen duerfen joinen.
-
-enforcement-enabled: true
-log-blocked-attempts: true
-kick-message: "&cBitte verbinde dich nur ueber den Proxy-Server. Direkte Unterserver-Joins sind blockiert."
-
-# Exakte Proxy-IPs
-allowed-proxy-ips:
- - "127.0.0.1"
- - "::1"
-
-# Optional ganze Netze im CIDR-Format
-allowed-proxy-cidrs: []
-# Beispiel:
-# allowed-proxy-cidrs:
-# - "10.0.0.0/24"
-# - "192.168.178.10/32"
-
-# Optional: zentrale Steuerung ueber StatusAPI
-# Standard bleibt Standalone (enabled=false), damit BackendJoinGuard auch ohne StatusAPI nutzbar ist.
-statusapi-sync:
- enabled: true
- base-url: "http://127.0.0.1:9191"
- endpoint-path: "/network/backendguard/config"
- api-key: "bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6"
- interval-seconds: 60
- log-sync-errors: true
+# BackendJoinGuard
+# Dieses Plugin kommt auf JEDEN Unterserver (Paper/Spigot), nicht auf den Proxy.
+# Nur Verbindungen von diesen Proxy-IPs oder Netzbereichen duerfen joinen.
+
+enforcement-enabled: true
+log-blocked-attempts: true
+kick-message: "&cBitte verbinde dich nur ueber den Proxy-Server. Direkte Unterserver-Joins sind blockiert."
+
+# Exakte Proxy-IPs
+allowed-proxy-ips:
+ - "127.0.0.1"
+ - "::1"
+
+# Optional ganze Netze im CIDR-Format
+allowed-proxy-cidrs: []
+# Beispiel:
+# allowed-proxy-cidrs:
+# - "10.0.0.0/24"
+# - "192.168.178.10/32"
+
+# Optional: zentrale Steuerung ueber StatusAPI
+# Standard bleibt Standalone (enabled=false), damit BackendJoinGuard auch ohne StatusAPI nutzbar ist.
+statusapi-sync:
+ enabled: true
+ base-url: "http://127.0.0.1:9191"
+ endpoint-path: "/network/backendguard/config"
+ api-key: "bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6"
+ interval-seconds: 60
+ log-sync-errors: true
diff --git a/backend-join-guard/src/main/resources/plugin.yml b/backend-join-guard/src/main/resources/plugin.yml
index b547e07..baf61cc 100644
--- a/backend-join-guard/src/main/resources/plugin.yml
+++ b/backend-join-guard/src/main/resources/plugin.yml
@@ -1,14 +1,14 @@
-name: BackendJoinGuard
-main: net.viper.backendguard.BackendJoinGuardPlugin
-version: 1.0.0
-author: M_Viper
-description: Blockiert direkte Joins auf Backendserver und erlaubt nur Proxy-IPs.
-api-version: '1.20'
-commands:
- backendguard:
- description: BackendJoinGuard neu laden
- usage: /backendguard reload
-permissions:
- backendguard.admin:
- description: Darf BackendJoinGuard verwalten
- default: op
+name: BackendJoinGuard
+main: net.viper.backendguard.BackendJoinGuardPlugin
+version: 1.0.0
+author: M_Viper
+description: Blockiert direkte Joins auf Backendserver und erlaubt nur Proxy-IPs.
+api-version: '1.20'
+commands:
+ backendguard:
+ description: BackendJoinGuard neu laden
+ usage: /backendguard reload
+permissions:
+ backendguard.admin:
+ description: Darf BackendJoinGuard verwalten
+ default: op