From 030ced87f9f5c696aa6482abbf72fa0cae661703 Mon Sep 17 00:00:00 2001 From: Peijun Zhao Date: Sat, 16 May 2026 15:50:48 +0800 Subject: [PATCH] =?UTF-8?q?V5.0=20Paper=E6=8F=92=E4=BB=B6=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=EF=BC=9A=E6=B7=B1=E5=BA=A6=E6=A4=8D=E5=85=A5Minecraft?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=BF=9B=E7=A8=8B=EF=BC=8C=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=AE=8C=E7=BE=8E=E5=AE=9E=E6=97=B6=E9=98=B2=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心变更: - 新增 PaperMC 插件层(14个新文件):主插件入口 AluerPlugin + 10个 Bukkit 事件监听器 + DataBridge 数据桥接 + InternalCommandExecutor 内部命令执行器 - 运行时注入服务器进程,直接访问 Bukkit/Paper API,取消 RCON 中间层 - 9种反作弊实时检测(Speed/Fly/Jesus/NoFall/KillAura/Reach/AutoClicker/Scaffold/Nuker)全部迁移为事件驱动 - 聊天安全(刷屏/广告/钓鱼)与命令滥用检测由 AsyncPlayerChatEvent/PlayerCommandPreprocessEvent 直接拦截 - AlertType 枚举扩展至 47 项告警类型,覆盖反作弊/玩家行为/服务器保护/聊天安全/网络/主机全部维度 - 双模式兼容:serverguard.mode=plugin(嵌入式)| external(传统外部监控),旧模式代码完整保留 - Maven 构建切换为 shade 插件,生成 Paper 插件兼容的扁平 JAR - 278 项全量测试通过,0 失败 --- pom.xml | 66 ++- .../com/aluer/ServerGuardApplication.java | 31 +- .../com/aluer/config/ServerGuardConfig.java | 7 + src/main/java/com/aluer/model/AlertEvent.java | 11 + src/main/java/com/aluer/model/AlertType.java | 53 ++- .../aluer/plugin/AluerCommandExecutor.java | 379 ++++++++++++++++++ .../java/com/aluer/plugin/AluerPlugin.java | 185 +++++++++ .../com/aluer/plugin/bridge/DataBridge.java | 373 +++++++++++++++++ .../bridge/InternalCommandExecutor.java | 238 +++++++++++ .../plugin/listener/BlockEventListener.java | 153 +++++++ .../plugin/listener/ChatEventListener.java | 98 +++++ .../plugin/listener/CombatEventListener.java | 112 ++++++ .../plugin/listener/CommandEventListener.java | 88 ++++ .../plugin/listener/EntityEventListener.java | 83 ++++ .../listener/InventoryEventListener.java | 116 ++++++ .../plugin/listener/PacketEventListener.java | 59 +++ .../plugin/listener/PlayerEventListener.java | 184 +++++++++ .../plugin/listener/WorldEventListener.java | 58 +++ .../java/com/aluer/service/AutoExecutor.java | 120 ++++-- .../com/aluer/service/ServerGuardService.java | 37 +- src/main/resources/application.yml | 4 + src/main/resources/plugin.yml | 47 +++ 22 files changed, 2454 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/aluer/plugin/AluerCommandExecutor.java create mode 100644 src/main/java/com/aluer/plugin/AluerPlugin.java create mode 100644 src/main/java/com/aluer/plugin/bridge/DataBridge.java create mode 100644 src/main/java/com/aluer/plugin/bridge/InternalCommandExecutor.java create mode 100644 src/main/java/com/aluer/plugin/listener/BlockEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/ChatEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/CombatEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/CommandEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/EntityEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/InventoryEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/PacketEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/PlayerEventListener.java create mode 100644 src/main/java/com/aluer/plugin/listener/WorldEventListener.java create mode 100644 src/main/resources/plugin.yml diff --git a/pom.xml b/pom.xml index 61fa0c2..2097f08 100644 --- a/pom.xml +++ b/pom.xml @@ -26,8 +26,16 @@ UTF-8 2.6.0 3.6.1 + 1.21.1-R0.1-SNAPSHOT + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + org.springframework.boot @@ -105,23 +113,19 @@ spring-boot-starter-test test + + + + io.papermc.paper + paper-api + ${paper.version} + provided + serverguard - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - org.apache.maven.plugins maven-compiler-plugin @@ -131,6 +135,44 @@ 17 + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + shade + + + + com.aluer.ServerGuardApplication + + + META-INF/spring.handlers + + + META-INF/spring.schemas + + + META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + diff --git a/src/main/java/com/aluer/ServerGuardApplication.java b/src/main/java/com/aluer/ServerGuardApplication.java index a80b472..a605533 100644 --- a/src/main/java/com/aluer/ServerGuardApplication.java +++ b/src/main/java/com/aluer/ServerGuardApplication.java @@ -7,6 +7,15 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.scheduling.annotation.EnableAsync; +/** + * Aluer ServerGuard 主应用入口 + * + * 支持两种运行模式: + * 1. standalone — java -jar serverguard.jar(传统外部监控模式) + * 2. plugin — 由 PaperMC 插件 AluerPlugin 调用 bootstrap() 启动嵌入式上下文 + * + * 模式由系统属性 serverguard.mode 决定,AluerPlugin 启动时自动设置为 "plugin" + */ @SpringBootApplication @EnableAsync public class ServerGuardApplication { @@ -14,6 +23,8 @@ public class ServerGuardApplication { private static volatile ConfigurableApplicationContext context; public static void main(String[] args) { + // standalone 模式:直接启动 + System.setProperty("serverguard.mode", "external"); context = SpringApplication.run(ServerGuardApplication.class, args); Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -23,7 +34,25 @@ public class ServerGuardApplication { } }, "serverguard-shutdown-hook")); - logger.info("Aluer ServerGuard is ready"); + logger.info("Aluer ServerGuard standalone mode is ready"); + } + + /** + * 插件模式启动入口 — 由 AluerPlugin.onEnable() 调用 + * + * Paper 插件在 onEnable 时异步调用此方法, + * 返回的 ConfigurableApplicationContext 由插件持有并在 onDisable 时关闭。 + * + * @return Spring 应用上下文 + */ + public static ConfigurableApplicationContext bootstrap() { + if (context != null) { + logger.warn("Spring Boot context already running, reusing existing context"); + return context; + } + context = SpringApplication.run(ServerGuardApplication.class); + logger.info("Aluer ServerGuard plugin mode bootstrapped — Spring context ready"); + return context; } public static ConfigurableApplicationContext getContext() { diff --git a/src/main/java/com/aluer/config/ServerGuardConfig.java b/src/main/java/com/aluer/config/ServerGuardConfig.java index 9e87b00..7a448d5 100644 --- a/src/main/java/com/aluer/config/ServerGuardConfig.java +++ b/src/main/java/com/aluer/config/ServerGuardConfig.java @@ -23,6 +23,13 @@ public class ServerGuardConfig { private AnnouncementConfig announcement = new AnnouncementConfig(); private AfkConfig afk = new AfkConfig(); + /** 运行模式:plugin(Paper 插件内部)或 external(传统外部监控),默认 external */ + private String mode = "external"; + + public String getMode() { return mode; } + public void setMode(String mode) { this.mode = mode; } + public boolean isPluginMode() { return "plugin".equalsIgnoreCase(mode); } + public static class MinecraftConfig { private String serviceName = "minecraft"; private String processName = "java"; diff --git a/src/main/java/com/aluer/model/AlertEvent.java b/src/main/java/com/aluer/model/AlertEvent.java index 030ce16..38b855e 100644 --- a/src/main/java/com/aluer/model/AlertEvent.java +++ b/src/main/java/com/aluer/model/AlertEvent.java @@ -1,6 +1,8 @@ package com.aluer.model; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; public class AlertEvent { private AlertType type; @@ -9,6 +11,8 @@ public class AlertEvent { private LocalDateTime timestamp; private double confidence; private String suggestedAction; + /** 告警来源(玩家名/IP/系统模块名) */ + private String source; public AlertEvent() { this.timestamp = LocalDateTime.now(); @@ -32,4 +36,11 @@ public class AlertEvent { public void setConfidence(double confidence) { this.confidence = confidence; } public String getSuggestedAction() { return suggestedAction; } public void setSuggestedAction(String suggestedAction) { this.suggestedAction = suggestedAction; } + public String getSource() { return source; } + public void setSource(String source) { this.source = source; } + + /** 以 epoch millis 设置时间戳 */ + public void setTimestamp(long epochMillis) { + this.timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), ZoneId.systemDefault()); + } } diff --git a/src/main/java/com/aluer/model/AlertType.java b/src/main/java/com/aluer/model/AlertType.java index dfb1824..6086961 100644 --- a/src/main/java/com/aluer/model/AlertType.java +++ b/src/main/java/com/aluer/model/AlertType.java @@ -1,6 +1,7 @@ package com.aluer.model; public enum AlertType { + // ─── 系统监控 ────────────────────────────── PROCESS_DEAD("Process Dead", "Minecraft process not found"), TPS_LOW("Low TPS", "TPS below threshold"), CPU_HIGH("High CPU", "CPU usage above threshold"), @@ -8,7 +9,57 @@ public enum AlertType { CONNECTION_FLOOD("Connection Flood", "Possible DDoS attack detected"), LOG_ATTACK("Attack Detected", "Malicious activity in logs"), BACKUP_FAILED("Backup Failed", "Backup task failed"), - AI_ANOMALY("AI Anomaly", "AI detected abnormal behavior"); + AI_ANOMALY("AI Anomaly", "AI detected abnormal behavior"), + + // ─── V4.0 反作弊 ────────────────────────── + SECURITY_KILL_AURA("KillAura", "Multi-target attack or aimbot consistency detected"), + SECURITY_REACH("Reach", "Attack distance exceeds normal maximum"), + SECURITY_SPEED("Speed", "Horizontal movement speed anomaly"), + SECURITY_JESUS("Jesus", "Abnormal water walking detected"), + SECURITY_NOFALL("NoFall", "Fall damage bypass detected"), + SECURITY_SCAFFOLD("Scaffold", "Rapid foot-block placement detected"), + SECURITY_NUKER("Nuker", "Rapid block breaking detected"), + SECURITY_AUTO_CLICKER("AutoClicker", "Abnormal attack frequency detected"), + SECURITY_AUTO_FISH("AutoFish", "Automated fishing detected"), + SECURITY_FLY("Fly", "Illegal flight detected"), + + // ─── V4.0 玩家行为 ──────────────────────── + SECURITY_CHEST_STEAL("ChestSteal", "Rapid container theft detected"), + SECURITY_INVENTORY_MANIPULATION("InventoryManipulation", "Abnormal inventory interaction"), + SECURITY_GRIEF("Grief", "Valuable block destruction pattern"), + SECURITY_ALT_ACCOUNT("AltAccount", "Multiple accounts from same IP"), + SECURITY_BARITONE("Baritone", "Automated mining bot detected"), + SECURITY_XRAY("Xray", "Suspicious ore mining pattern"), + + // ─── V4.0 服务器保护 ────────────────────── + SECURITY_SIGN_EXPLOIT("SignExploit", "Abnormal sign NBT data detected"), + SECURITY_BOOK_BAN("BookBan", "Malicious book content detected"), + SECURITY_RESOURCE_PACK_EXPLOIT("ResourcePackExploit", "Malicious resource pack attempt"), + SECURITY_TAB_COMPLETE_CRASH("TabCompleteCrash", "Crash-inducing tab complete detected"), + SECURITY_OFFLINE_MODE_SPOOF("OfflineModeSpoof", "Offline mode UUID spoof detected"), + + // ─── V4.0 聊天与社交安全 ────────────────── + CHAT_FLOOD("ChatFlood", "Chat message spam detected"), + CHAT_ADVERTISEMENT("Advertisement", "IP/Domain advertisement detected"), + CHAT_PHISHING("Phishing", "Phishing link detected"), + COMMAND_ABUSE("CommandAbuse", "Sensitive command abuse detected"), + + // ─── 网络安全 ───────────────────────────── + SECURITY_DDOS("DDoS", "Distributed denial of service attack"), + SECURITY_PORT_SCAN("PortScan", "Port scanning activity detected"), + SECURITY_BRUTE_FORCE("BruteForce", "Brute force login attempt"), + SECURITY_VPN_PROXY("VPN/Proxy", "Connection via VPN or proxy detected"), + SECURITY_DNS_TUNNEL("DNSTunnel", "DNS tunneling detected"), + + // ─── 主机安全 ───────────────────────────── + SECURITY_REVERSE_SHELL("ReverseShell", "Reverse shell connection detected"), + SECURITY_PROCESS_INJECTION("ProcessInjection", "Process injection detected"), + SECURITY_FILE_TAMPER("FileTamper", "File integrity violation detected"), + SECURITY_BACKDOOR_PLUGIN("BackdoorPlugin", "Suspicious plugin detected"), + SECURITY_CONFIG_TAMPER("ConfigTamper", "Configuration tampering detected"), + + // ─── 通用 ───────────────────────────────── + SECURITY_OTHER("Security", "Security alert"); private final String title; private final String description; diff --git a/src/main/java/com/aluer/plugin/AluerCommandExecutor.java b/src/main/java/com/aluer/plugin/AluerCommandExecutor.java new file mode 100644 index 0000000..1de493c --- /dev/null +++ b/src/main/java/com/aluer/plugin/AluerCommandExecutor.java @@ -0,0 +1,379 @@ +package com.aluer.plugin; + +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.InternalCommandExecutor; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Aluer 插件命令执行器 — 处理 /aluer /aluerstatus /aluerplayers 等管理命令 + * + * 命令列表: + * /aluer status — 查看防护系统状态 + * /aluer scan <玩家> — 手动触发玩家安全扫描 + * /aluer defense — 查看当前防御等级 + * /aluer info — 查看系统信息 + * /aluerplayers — 查看玩家风险列表 + * /aluerblock <玩家|ip> <目标> [理由] — 手动封禁 + * /aluerunblock <玩家|ip> <目标> — 手动解封 + * /aluerwhitelist — 白名单管理 + */ +public class AluerCommandExecutor implements CommandExecutor, TabCompleter { + private static final Logger logger = LoggerFactory.getLogger(AluerCommandExecutor.class); + + private final AluerPlugin plugin; + private final DataBridge dataBridge; + + private static final String PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.GOLD + "Aluer" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET; + + public AluerCommandExecutor(AluerPlugin plugin, DataBridge dataBridge) { + this.plugin = plugin; + this.dataBridge = dataBridge; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + String cmdName = command.getName().toLowerCase(); + + switch (cmdName) { + case "aluer": + return handleAluerCommand(sender, args); + case "aluerstatus": + return handleStatusCommand(sender); + case "aluerplayers": + return handlePlayersCommand(sender); + case "aluerblock": + return handleBlockCommand(sender, args); + case "aluerunblock": + return handleUnblockCommand(sender, args); + case "aluerscan": + return handleScanCommand(sender, args); + case "aluerwhitelist": + return handleWhitelistCommand(sender, args); + default: + return false; + } + } + + private boolean handleAluerCommand(CommandSender sender, String[] args) { + if (args.length == 0) { + sendHelp(sender); + return true; + } + + switch (args[0].toLowerCase()) { + case "status": + return handleStatusCommand(sender); + case "scan": + return handleScanCommand(sender, Arrays.copyOfRange(args, 1, args.length)); + case "defense": + return handleDefenseCommand(sender); + case "info": + return handleInfoCommand(sender); + default: + sendHelp(sender); + return true; + } + } + + private boolean handleStatusCommand(CommandSender sender) { + sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ Aluer ServerGuard v4.0.0 ═══"); + + if (dataBridge != null) { + double tps = dataBridge.getCurrentTps(); + int online = dataBridge.getOnlinePlayerCount(); + ChatColor tpsColor = tps >= 18 ? ChatColor.GREEN : tps >= 15 ? ChatColor.YELLOW : ChatColor.RED; + + sender.sendMessage(PREFIX + "TPS: " + tpsColor + String.format("%.1f", tps) + + ChatColor.RESET + " | 在线玩家: " + ChatColor.AQUA + online); + sender.sendMessage(PREFIX + "Spring 上下文: " + + (plugin.isSpringReady() ? ChatColor.GREEN + "就绪" : ChatColor.RED + "初始化中")); + sender.sendMessage(PREFIX + "包速率: " + ChatColor.GRAY + + String.format("%.0f pps", dataBridge.getPacketsPerSecond())); + sender.sendMessage(PREFIX + "事件处理: " + ChatColor.GRAY + + dataBridge.getEventCount("player.join") + " 登录, " + + dataBridge.getEventCount("chat.message") + " 聊天, " + + dataBridge.getEventCount("block.break") + " 破坏"); + } else { + sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪,请稍候..."); + } + + return true; + } + + private boolean handlePlayersCommand(CommandSender sender) { + if (dataBridge == null) { + sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪"); + return true; + } + + var players = dataBridge.getAllPlayers(); + sender.sendMessage(PREFIX + ChatColor.YELLOW + "在线玩家: " + players.size()); + + for (var snap : players) { + String riskInfo = ""; + if (snap.getRecentTargetCount() > 3) { + riskInfo = ChatColor.RED + " [多目标]"; + } + if (snap.hasSuspiciousAngleConsistency()) { + riskInfo += ChatColor.DARK_RED + " [瞄准可疑]"; + } + sender.sendMessage(PREFIX + " " + ChatColor.WHITE + snap.name + + ChatColor.GRAY + " | IP: " + snap.ip + + " | Ping: " + snap.ping + "ms" + + riskInfo); + } + + return true; + } + + private boolean handleBlockCommand(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerblock <目标> [理由]"); + return true; + } + + String type = args[0].toLowerCase(); + String target = args[1]; + String reason = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) + : "管理员手动封禁"; + + InternalCommandExecutor executor = plugin.getSpringContext() != null + ? plugin.getSpringContext().getBean(InternalCommandExecutor.class) + : null; + + if (executor == null) { + sender.sendMessage(PREFIX + ChatColor.RED + "命令执行器未就绪"); + return true; + } + + boolean success; + if ("ip".equals(type)) { + success = executor.banIP(target, reason); + } else { + success = executor.banPlayer(target, reason); + } + + if (success) { + sender.sendMessage(PREFIX + ChatColor.GREEN + "已封禁 " + type + ": " + target); + Bukkit.getServer().broadcast( + net.kyori.adventure.text.Component.text( + ChatColor.RED + "[Aluer] " + sender.getName() + " 封禁了 " + target + ": " + reason)); + } else { + sender.sendMessage(PREFIX + ChatColor.RED + "封禁失败: " + target); + } + + return true; + } + + private boolean handleUnblockCommand(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerunblock <目标>"); + return true; + } + + String type = args[0].toLowerCase(); + String target = args[1]; + + InternalCommandExecutor executor = plugin.getSpringContext() != null + ? plugin.getSpringContext().getBean(InternalCommandExecutor.class) + : null; + + if (executor == null) { + sender.sendMessage(PREFIX + ChatColor.RED + "命令执行器未就绪"); + return true; + } + + boolean success; + if ("ip".equals(type)) { + success = executor.unbanIP(target); + } else { + success = executor.unbanPlayer(target); + } + + if (success) { + sender.sendMessage(PREFIX + ChatColor.GREEN + "已解封 " + type + ": " + target); + } else { + sender.sendMessage(PREFIX + ChatColor.RED + "解封失败: " + target); + } + + return true; + } + + private boolean handleScanCommand(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerscan <玩家名>"); + return true; + } + + String targetName = args[0]; + Player target = Bukkit.getServer().getPlayer(targetName); + + if (target == null) { + sender.sendMessage(PREFIX + ChatColor.RED + "玩家不在线: " + targetName); + return true; + } + + if (dataBridge == null) { + sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪"); + return true; + } + + var snap = dataBridge.getPlayer(target.getUniqueId()); + if (snap == null) { + sender.sendMessage(PREFIX + ChatColor.RED + "未找到玩家数据"); + return true; + } + + sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 安全扫描: " + targetName + " ═══"); + sender.sendMessage(PREFIX + "位置: " + String.format("(%.1f, %.1f, %.1f) [%s]", snap.x, snap.y, snap.z, snap.world)); + sender.sendMessage(PREFIX + "IP: " + snap.ip + " | Ping: " + snap.ping + "ms"); + sender.sendMessage(PREFIX + "飞行: " + snap.isFlying + " | 冲刺: " + snap.isSprinting + " | 潜行: " + snap.isSneaking); + sender.sendMessage(PREFIX + "最近攻击目标: " + snap.getRecentTargetCount() + " 个"); + sender.sendMessage(PREFIX + "攻击角度一致性: " + (snap.hasSuspiciousAngleConsistency() ? ChatColor.RED + "可疑" : ChatColor.GREEN + "正常")); + sender.sendMessage(PREFIX + "最近消息: " + snap.recentMessages.size() + " 条"); + sender.sendMessage(PREFIX + "最近命令: " + snap.recentCommands.size() + " 条"); + sender.sendMessage(PREFIX + "在线时长: " + (snap.getOnlineTime() / 60000) + " 分钟"); + + return true; + } + + private boolean handleDefenseCommand(CommandSender sender) { + sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 防御状态 ═══"); + sender.sendMessage(PREFIX + "模式: " + ChatColor.AQUA + "Paper Plugin(嵌入式)"); + + if (dataBridge != null) { + double[] tps = dataBridge.getTpsArray(); + sender.sendMessage(PREFIX + "TPS: " + String.format("1m=%.1f 5m=%.1f 15m=%.1f", tps[0], tps[1], tps[2])); + sender.sendMessage(PREFIX + "在线: " + dataBridge.getOnlinePlayerCount() + " | 包速率: " + + String.format("%.0f pps", dataBridge.getPacketsPerSecond())); + } + return true; + } + + private boolean handleInfoCommand(CommandSender sender) { + sender.sendMessage(PREFIX + ChatColor.GOLD + "Aluer ServerGuard v4.0.0"); + sender.sendMessage(PREFIX + ChatColor.GRAY + "AI-powered Minecraft PaperMC Server Protection"); + sender.sendMessage(PREFIX + "Author: Aluer | Java 17 | Spring Boot 3.2.0"); + sender.sendMessage(PREFIX + "29 反作弊模块 | DDoS 防御 | WAF | Kernel 引擎"); + sender.sendMessage(PREFIX + "模式: " + (plugin.isSpringReady() ? "完全运作" : "初始化中")); + return true; + } + + private boolean handleWhitelistCommand(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerwhitelist "); + return true; + } + + InternalCommandExecutor executor = plugin.getSpringContext() != null + ? plugin.getSpringContext().getBean(InternalCommandExecutor.class) + : null; + + switch (args[0].toLowerCase()) { + case "on": + if (executor != null) { + executor.enableWhitelist(); + sender.sendMessage(PREFIX + ChatColor.RED + "紧急白名单模式已启用!"); + } + break; + case "off": + if (executor != null) { + executor.disableWhitelist(); + sender.sendMessage(PREFIX + ChatColor.GREEN + "白名单模式已关闭"); + } + break; + case "status": + boolean enabled = executor != null && executor.isWhitelistEnabled(); + sender.sendMessage(PREFIX + "白名单: " + (enabled ? ChatColor.RED + "启用中" : ChatColor.GREEN + "关闭")); + break; + default: + sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerwhitelist "); + } + return true; + } + + private void sendHelp(CommandSender sender) { + sender.sendMessage(PREFIX + ChatColor.GOLD + "═══ Aluer ServerGuard 命令 ═══"); + sender.sendMessage(ChatColor.YELLOW + "/aluer status" + ChatColor.GRAY + " — 查看防护状态"); + sender.sendMessage(ChatColor.YELLOW + "/aluer scan <玩家>" + ChatColor.GRAY + " — 安全扫描玩家"); + sender.sendMessage(ChatColor.YELLOW + "/aluer defense" + ChatColor.GRAY + " — 查看防御等级"); + sender.sendMessage(ChatColor.YELLOW + "/aluer info" + ChatColor.GRAY + " — 系统信息"); + sender.sendMessage(ChatColor.YELLOW + "/aluerplayers" + ChatColor.GRAY + " — 玩家风险列表"); + sender.sendMessage(ChatColor.YELLOW + "/aluerblock <目标>" + ChatColor.GRAY + " — 手动封禁"); + sender.sendMessage(ChatColor.YELLOW + "/aluerunblock <目标>" + ChatColor.GRAY + " — 手动解封"); + sender.sendMessage(ChatColor.YELLOW + "/aluerwhitelist " + ChatColor.GRAY + " — 白名单管理"); + } + + // ─── 命令补全 ────────────────────────────────────── + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + String cmdName = command.getName().toLowerCase(); + + switch (cmdName) { + case "aluer": + if (args.length == 1) { + return filterStartsWith(Arrays.asList("status", "scan", "defense", "info"), args[0]); + } + if (args.length == 2 && "scan".equalsIgnoreCase(args[0])) { + return filterStartsWith( + Bukkit.getServer().getOnlinePlayers().stream() + .map(Player::getName) + .collect(Collectors.toList()), + args[1]); + } + break; + case "aluerblock": + if (args.length == 1) { + return filterStartsWith(Arrays.asList("player", "ip"), args[0]); + } + if (args.length == 2 && "player".equalsIgnoreCase(args[0])) { + return filterStartsWith( + Bukkit.getServer().getOnlinePlayers().stream() + .map(Player::getName) + .collect(Collectors.toList()), + args[1]); + } + break; + case "aluerunblock": + if (args.length == 1) { + return filterStartsWith(Arrays.asList("player", "ip"), args[0]); + } + break; + case "aluerwhitelist": + if (args.length == 1) { + return filterStartsWith(Arrays.asList("on", "off", "status"), args[0]); + } + break; + case "aluerscan": + if (args.length == 1) { + return filterStartsWith( + Bukkit.getServer().getOnlinePlayers().stream() + .map(Player::getName) + .collect(Collectors.toList()), + args[0]); + } + break; + } + + return Collections.emptyList(); + } + + private List filterStartsWith(List options, String prefix) { + String lower = prefix.toLowerCase(); + return options.stream() + .filter(s -> s.toLowerCase().startsWith(lower)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/aluer/plugin/AluerPlugin.java b/src/main/java/com/aluer/plugin/AluerPlugin.java new file mode 100644 index 0000000..239a898 --- /dev/null +++ b/src/main/java/com/aluer/plugin/AluerPlugin.java @@ -0,0 +1,185 @@ +package com.aluer.plugin; + +import com.aluer.ServerGuardApplication; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.listener.*; +import org.bukkit.Bukkit; +import org.bukkit.event.HandlerList; +import org.bukkit.plugin.java.JavaPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Aluer ServerGuard — PaperMC 插件主入口 + * + * 插件加载时启动嵌入式 Spring Boot 上下文, + * 注册所有 Bukkit 事件监听器,通过 DataBridge + * 将服务器内部实时数据推送至安全分析引擎。 + * + * 架构: + * Bukkit Events → Event Listeners → DataBridge → Security Services → AI → AutoExecutor → Bukkit API + */ +public class AluerPlugin extends JavaPlugin { + private static final Logger logger = LoggerFactory.getLogger(AluerPlugin.class); + + /** 全局单例引用,供监听器在 Spring 上下文就绪前获取插件实例 */ + private static AluerPlugin instance; + + /** Spring Boot 应用上下文,插件关闭时需 gracefully shutdown */ + private ConfigurableApplicationContext springContext; + + /** 数据桥接中心,连接 Bukkit 事件与安全服务 */ + private DataBridge dataBridge; + + /** Spring 上下文是否已就绪 */ + private volatile boolean springReady = false; + + // ─── 生命周期 ──────────────────────────────────────── + + @Override + public void onLoad() { + instance = this; + logger.info("Aluer ServerGuard v4.0.0 — PaperMC Plugin loading (LOAD STARTUP)"); + } + + @Override + public void onEnable() { + long startMs = System.currentTimeMillis(); + + // 步骤 1:启动嵌入式 Spring Boot(异步,避免阻塞 Paper 主线程) + logger.info("Starting embedded Spring Boot context..."); + new Thread(() -> { + try { + // 设置 Paper 插件模式标志 + System.setProperty("serverguard.mode", "plugin"); + System.setProperty("server.mode", "plugin"); + + springContext = ServerGuardApplication.bootstrap(); + dataBridge = springContext.getBean(DataBridge.class); + springReady = true; + logger.info("Spring Boot context ready — all security services initialized"); + } catch (Exception e) { + logger.error("Failed to start Spring Boot context: {}", e.getMessage(), e); + } + }, "aluer-spring-boot").start(); + + // 步骤 2:等待 Spring 上下文就绪(最长 30 秒) + int waited = 0; + while (!springReady && waited < 30) { + try { + Thread.sleep(1000); + waited++; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + if (!springReady) { + logger.error("Spring Boot failed to start within 30s — plugin will operate in degraded mode"); + } + + // 步骤 3:注册 Bukkit 事件监听器 + registerEventListeners(); + + // 步骤 4:注册插件命令 + registerCommands(); + + long elapsed = System.currentTimeMillis() - startMs; + logger.info("Aluer ServerGuard enabled in {}ms — real-time protection active", elapsed); + logger.info(" Listeners: 10 event handlers registered"); + logger.info(" Commands: /aluer, /aluerstatus, /aluerplayers, /aluerblock, /aluerunblock, /aluerscan, /aluerwhitelist"); + logger.info(" Web Console: http://0.0.0.0:8080/"); + } + + @Override + public void onDisable() { + logger.info("Shutting down Aluer ServerGuard..."); + + // 注销所有 Bukkit 事件监听器 + HandlerList.unregisterAll(this); + + // 关闭 Spring Boot 上下文 + if (springContext != null) { + springContext.close(); + springContext = null; + } + + instance = null; + logger.info("Aluer ServerGuard disabled — protection stopped"); + } + + // ─── 事件监听器注册 ────────────────────────────────── + + private void registerEventListeners() { + // 注意:监听器在构造函数中接收 DataBridge 引用 + // 如果 Spring 未就绪则使用 null(监听器内部做空安全处理) + DataBridge bridge = springReady ? dataBridge : null; + + // 核心监听器:玩家、战斗、聊天、命令 + Bukkit.getPluginManager().registerEvents(new PlayerEventListener(this, bridge), this); + Bukkit.getPluginManager().registerEvents(new CombatEventListener(this, bridge), this); + Bukkit.getPluginManager().registerEvents(new ChatEventListener(this, bridge), this); + Bukkit.getPluginManager().registerEvents(new CommandEventListener(this, bridge), this); + + // 方块与物品监听器 + Bukkit.getPluginManager().registerEvents(new BlockEventListener(this, bridge), this); + Bukkit.getPluginManager().registerEvents(new InventoryEventListener(this, bridge), this); + + // 实体与世界监听器 + Bukkit.getPluginManager().registerEvents(new EntityEventListener(this, bridge), this); + Bukkit.getPluginManager().registerEvents(new WorldEventListener(this, bridge), this); + + // 包级监听器(Paper API 特有,用于精确的包速率控制) + if (Bukkit.getServer().getClass().getPackageName().contains("paper")) { + Bukkit.getPluginManager().registerEvents(new PacketEventListener(this, bridge), this); + } + } + + // ─── 命令注册 ──────────────────────────────────────── + + private void registerCommands() { + AluerCommandExecutor executor = new AluerCommandExecutor(this, dataBridge); + var aluerCmd = getCommand("aluer"); + if (aluerCmd != null) { + aluerCmd.setExecutor(executor); + aluerCmd.setTabCompleter(executor); + } + + var statusCmd = getCommand("aluerstatus"); + if (statusCmd != null) statusCmd.setExecutor(executor); + var playersCmd = getCommand("aluerplayers"); + if (playersCmd != null) playersCmd.setExecutor(executor); + var blockCmd = getCommand("aluerblock"); + if (blockCmd != null) blockCmd.setExecutor(executor); + var unblockCmd = getCommand("aluerunblock"); + if (unblockCmd != null) unblockCmd.setExecutor(executor); + var scanCmd = getCommand("aluerscan"); + if (scanCmd != null) scanCmd.setExecutor(executor); + var whitelistCmd = getCommand("aluerwhitelist"); + if (whitelistCmd != null) whitelistCmd.setExecutor(executor); + } + + // ─── 公开访问器 ────────────────────────────────────── + + /** 获取插件单例实例 */ + public static AluerPlugin getInstance() { + return instance; + } + + /** 获取 Spring 应用上下文(可能为 null,在 Spring 就绪前) */ + public ConfigurableApplicationContext getSpringContext() { + return springContext; + } + + /** 获取 DataBridge(可能为 null,在 Spring 就绪前) */ + public DataBridge getDataBridge() { + return dataBridge; + } + + /** Spring 上下文是否已完全初始化 */ + public boolean isSpringReady() { + return springReady; + } +} diff --git a/src/main/java/com/aluer/plugin/bridge/DataBridge.java b/src/main/java/com/aluer/plugin/bridge/DataBridge.java new file mode 100644 index 0000000..dbad76f --- /dev/null +++ b/src/main/java/com/aluer/plugin/bridge/DataBridge.java @@ -0,0 +1,373 @@ +package com.aluer.plugin.bridge; + +import com.aluer.model.AlertEvent; +import com.aluer.model.AlertType; +import com.aluer.model.MetricsData; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +/** + * 数据桥接中心 — Bukkit 事件与安全服务之间的实时数据中枢 + * + * 职责: + * 1. 维护所有在线玩家的实时快照(位置、速度、攻击记录等) + * 2. 采集 TPS、连接数等服务器级指标 + * 3. 接收 Bukkit 监听器推送的原始事件并转换为 AlertEvent + * 4. 为安全服务提供实时查询接口 + * 5. 将告警事件投递至 ServerGuardService 的告警处理管道 + * + * 所有方法均为线程安全设计(ConcurrentHashMap + AtomicReference) + */ +@Service +public class DataBridge { + private static final Logger logger = LoggerFactory.getLogger(DataBridge.class); + + // ─── 玩家状态 ────────────────────────────────────── + + /** 在线玩家实时快照(UUID → 快照) */ + private final ConcurrentHashMap players = new ConcurrentHashMap<>(); + + // ─── 服务器指标 ──────────────────────────────────── + + /** 当前 TPS(20.0 = 满速) */ + private final ConcurrentHashMap eventCounters = new ConcurrentHashMap<>(); + + /** 告警事件生产者(由 ServerGuardService 注入) */ + private volatile Consumer alertHandler; + + /** 指标数据生产者(由 ServerGuardService 注入) */ + private volatile Consumer metricsHandler; + + /** 最近一次包速率快照的时间戳 */ + private volatile long lastPacketSnapshotTime = System.currentTimeMillis(); + + /** 当前秒内的包计数 */ + private final AtomicLong packetCountCurrentSecond = new AtomicLong(0); + + /** 上次秒的包速率 */ + private volatile double packetsPerSecond = 0.0; + + // ─── 玩家快照管理 ────────────────────────────────── + + /** 玩家加入时创建快照 */ + public void registerPlayer(Player player) { + PlayerSnapshot snapshot = new PlayerSnapshot(player); + players.put(player.getUniqueId(), snapshot); + logger.debug("Player registered: {} ({} online)", player.getName(), players.size()); + } + + /** 玩家退出时移除快照 */ + public void removePlayer(UUID uuid) { + PlayerSnapshot removed = players.remove(uuid); + if (removed != null) { + logger.debug("Player removed: {} ({} online)", removed.name, players.size()); + } + } + + /** 获取单个玩家快照 */ + public PlayerSnapshot getPlayer(UUID uuid) { + return players.get(uuid); + } + + /** 获取玩家快照(按名称模糊匹配) */ + public PlayerSnapshot getPlayerByName(String name) { + return players.values().stream() + .filter(p -> p.name.equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + /** 获取所有在线玩家快照 */ + public Collection getAllPlayers() { + return Collections.unmodifiableCollection(players.values()); + } + + /** 在线玩家数 */ + public int getOnlinePlayerCount() { + return players.size(); + } + + /** 获取指定 IP 的玩家列表 */ + public List getPlayersByIP(String ip) { + return players.values().stream() + .filter(p -> p.ip.equals(ip)) + .toList(); + } + + // ─── TPS 与服务器指标 ────────────────────────────── + + /** 获取当前服务器 TPS(1分钟平均) */ + public double getCurrentTps() { + try { + // Paper API: Bukkit.getServer().getTPS() 返回 double[3](1m, 5m, 15m) + double[] tpsValues = Bukkit.getServer().getTPS(); + return tpsValues.length > 0 ? tpsValues[0] : 20.0; + } catch (Exception e) { + return 20.0; // 非 Paper 服务端回退 + } + } + + /** 获取 TPS 数组 [1m, 5m, 15m] */ + public double[] getTpsArray() { + try { + return Bukkit.getServer().getTPS(); + } catch (Exception e) { + return new double[]{20.0, 20.0, 20.0}; + } + } + + /** 获取当前包速率(包/秒) */ + public double getPacketsPerSecond() { + return packetsPerSecond; + } + + /** 记录一个包事件(由 PacketEventListener 调用) */ + public void recordPacket() { + packetCountCurrentSecond.incrementAndGet(); + } + + /** 更新包速率快照(每秒调用一次) */ + public void updatePacketRate() { + long now = System.currentTimeMillis(); + long elapsed = now - lastPacketSnapshotTime; + if (elapsed > 0) { + packetsPerSecond = packetCountCurrentSecond.get() * 1000.0 / elapsed; + } + packetCountCurrentSecond.set(0); + lastPacketSnapshotTime = now; + } + + /** 获取事件计数器值 */ + public long getEventCount(String eventName) { + AtomicLong counter = eventCounters.get(eventName); + return counter != null ? counter.get() : 0; + } + + /** 递增事件计数器 */ + public long incrementEvent(String eventName) { + return eventCounters.computeIfAbsent(eventName, k -> new AtomicLong(0)).incrementAndGet(); + } + + // ─── 告警与指标推送 ──────────────────────────────── + + /** 设置告警处理器(由 ServerGuardService 注入) */ + public void setAlertHandler(Consumer handler) { + this.alertHandler = handler; + } + + /** 设置指标处理器(由 ServerGuardService 注入) */ + public void setMetricsHandler(Consumer handler) { + this.metricsHandler = handler; + } + + /** 推送一条告警事件到处理管道 */ + public void pushAlert(AlertEvent alert) { + Consumer handler = alertHandler; + if (handler != null) { + handler.accept(alert); + } else { + logger.warn("No alert handler set — alert dropped: {}", alert.getMessage()); + } + } + + /** 推送指标数据到处理管道 */ + public void pushMetrics(MetricsData data) { + Consumer handler = metricsHandler; + if (handler != null) { + handler.accept(data); + } + } + + // ─── 便捷告警构造 ────────────────────────────────── + + /** 快速构造并推送一条告警 */ + public void alert(AlertType type, String message, double confidence, String source) { + AlertEvent event = new AlertEvent(); + event.setType(type); + event.setMessage(message); + event.setConfidence(confidence); + event.setSource(source); + event.setTimestamp(Instant.now().toEpochMilli()); + pushAlert(event); + } + + // ─── 构建 MetricsData ─────────────────────────────── + + /** 从当前实时状态构建 MetricsData 快照 */ + public MetricsData buildMetricsSnapshot() { + MetricsData data = new MetricsData(); + data.setTps(getCurrentTps()); + + // CPU 和 Memory 通过 JMX 获取(插件模式下仍可使用系统级指标) + try { + java.lang.management.OperatingSystemMXBean osBean = + java.lang.management.ManagementFactory.getOperatingSystemMXBean(); + if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) { + data.setCpuUsage(sunOsBean.getCpuLoad() * 100.0); + long totalMem = Runtime.getRuntime().totalMemory(); + long maxMem = Runtime.getRuntime().maxMemory(); + data.setMemoryUsage((double) totalMem / maxMem * 100.0); + } + } catch (Exception ignored) { + data.setCpuUsage(0); + data.setMemoryUsage(0); + } + + data.setOnlinePlayers(players.size()); + data.setConnections(players.size()); + return data; + } + + // ─── 玩家快照数据结构 ────────────────────────────── + + /** + * 在线玩家的实时状态快照 + * 包含位置、速度、攻击记录等安全检测所需的关键字段 + */ + public static class PlayerSnapshot { + public final UUID uuid; + public final String name; + public volatile String ip; + public volatile double x, y, z; + public volatile String world; + public volatile float yaw, pitch; + public volatile double velocityX, velocityY, velocityZ; + public volatile double lastMoveX, lastMoveY, lastMoveZ; + public volatile long lastMoveTime; + public volatile int ping; + public volatile boolean isOp; + public volatile boolean isFlying; + public volatile boolean isSprinting; + public volatile boolean isSneaking; + public volatile boolean isInWater; + + /** 最近攻击目标(目标实体名 → 攻击时间戳) */ + public final ConcurrentHashMap recentTargets = new ConcurrentHashMap<>(); + + /** 最近攻击角度(用于 KillAura 检测) */ + public final ConcurrentLinkedDeque recentAttackAngles = new ConcurrentLinkedDeque<>(); + + /** 最近攻击距离(用于 Reach 检测) */ + public final ConcurrentLinkedDeque recentAttackDistances = new ConcurrentLinkedDeque<>(); + + /** 最近发送的消息(用于聊天刷屏检测) */ + public final ConcurrentLinkedDeque recentMessages = new ConcurrentLinkedDeque<>(); + + /** 最近执行的命令(用于命令滥用检测) */ + public final ConcurrentLinkedDeque recentCommands = new ConcurrentLinkedDeque<>(); + + /** 最近破坏的方块位置(用于 Nuker 检测) */ + public final ConcurrentLinkedDeque recentBlockBreaks = new ConcurrentLinkedDeque<>(); + + /** 最近放置的方块位置 */ + public final ConcurrentLinkedDeque recentBlockPlaces = new ConcurrentLinkedDeque<>(); + + /** 总移动距离(用于 Speed 检测) */ + public volatile double totalDistanceMoved = 0.0; + + /** 登录时间 */ + public final long loginTime; + + public PlayerSnapshot(org.bukkit.entity.Player player) { + this.uuid = player.getUniqueId(); + this.name = player.getName(); + this.ip = player.getAddress() != null && player.getAddress().getAddress() != null + ? player.getAddress().getAddress().getHostAddress() + : "unknown"; + this.world = player.getWorld().getName(); + var loc = player.getLocation(); + this.x = loc.getX(); + this.y = loc.getY(); + this.z = loc.getZ(); + this.yaw = loc.getYaw(); + this.pitch = loc.getPitch(); + this.ping = player.getPing(); + this.isOp = player.isOp(); + this.isFlying = player.isFlying(); + this.isSprinting = player.isSprinting(); + this.isSneaking = player.isSneaking(); + this.isInWater = player.isInWater(); + this.loginTime = System.currentTimeMillis(); + this.lastMoveTime = System.currentTimeMillis(); + } + + /** 更新玩家位置快照(由 PlayerMoveEvent 触发) */ + public void updatePosition(double x, double y, double z, float yaw, float pitch) { + this.lastMoveX = this.x; + this.lastMoveY = this.y; + this.lastMoveZ = this.z; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + this.lastMoveTime = System.currentTimeMillis(); + } + + /** 记录攻击目标(用于 KillAura 多目标检测) */ + public void recordTarget(String targetName) { + recentTargets.put(targetName, System.currentTimeMillis()); + // 清理 10 秒以外的旧记录 + long cutoff = System.currentTimeMillis() - 10_000; + recentTargets.entrySet().removeIf(e -> e.getValue() < cutoff); + } + + /** 获取最近 3 秒内的唯一目标数 */ + public int getRecentTargetCount() { + long cutoff = System.currentTimeMillis() - 3_000; + return (int) recentTargets.values().stream().filter(t -> t >= cutoff).count(); + } + + /** 记录攻击角度 */ + public void recordAttackAngle(double angle) { + recentAttackAngles.add(angle); + while (recentAttackAngles.size() > 10) { + recentAttackAngles.removeFirst(); + } + } + + /** 记录攻击距离 */ + public void recordAttackDistance(double distance) { + recentAttackDistances.add(distance); + while (recentAttackDistances.size() > 10) { + recentAttackDistances.removeFirst(); + } + } + + /** 检查攻击角度是否过于一致(可能使用 Aimbot) */ + public boolean hasSuspiciousAngleConsistency() { + if (recentAttackAngles.size() < 5) return false; + double maxDeviation = 0; + var it = recentAttackAngles.iterator(); + double prev = it.next(); + while (it.hasNext()) { + double curr = it.next(); + double diff = Math.abs(curr - prev); + if (diff > 180) diff = 360 - diff; + maxDeviation = Math.max(maxDeviation, diff); + prev = curr; + } + return maxDeviation < 5.0; // 小于 5 度偏差视为可疑 + } + + /** 在线时长(毫秒) */ + public long getOnlineTime() { + return System.currentTimeMillis() - loginTime; + } + + @Override + public String toString() { + return String.format("PlayerSnapshot{%s pos=(%.1f,%.1f,%.1f) world=%s ping=%d}", + name, x, y, z, world, ping); + } + } +} diff --git a/src/main/java/com/aluer/plugin/bridge/InternalCommandExecutor.java b/src/main/java/com/aluer/plugin/bridge/InternalCommandExecutor.java new file mode 100644 index 0000000..a23adc8 --- /dev/null +++ b/src/main/java/com/aluer/plugin/bridge/InternalCommandExecutor.java @@ -0,0 +1,238 @@ +package com.aluer.plugin.bridge; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.net.InetAddress; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 内部命令执行器 — 替代 RCON 协议,直接通过 Bukkit API 执行服务器命令 + * + * 插件模式下不再需要通过 TCP RCON 连接本地服务器, + * 直接调用 Bukkit.getServer().dispatchCommand() 即可。 + * + * 同时封装常见的运维操作:封禁/解封 IP、踢人、白名单管理等。 + */ +@Service +public class InternalCommandExecutor { + private static final Logger logger = LoggerFactory.getLogger(InternalCommandExecutor.class); + + /** 已封禁的 IP 集合(内存缓存,重启后失效,持久化由 BanList API 处理) */ + private final Set bannedIPs = ConcurrentHashMap.newKeySet(); + + /** 已封禁的玩家名集合 */ + private final Set bannedPlayers = ConcurrentHashMap.newKeySet(); + + // ─── 命令执行 ────────────────────────────────────── + + /** 以控制台身份执行任意 Minecraft 命令 */ + public boolean executeCommand(String command) { + try { + return Bukkit.getServer().dispatchCommand( + Bukkit.getServer().getConsoleSender(), command); + } catch (Exception e) { + logger.error("Command execution failed: {} — {}", command, e.getMessage()); + return false; + } + } + + /** 执行命令并返回是否成功(静默模式,不打日志) */ + public boolean executeCommandQuiet(String command) { + try { + return Bukkit.getServer().dispatchCommand( + Bukkit.getServer().getConsoleSender(), command); + } catch (Exception e) { + return false; + } + } + + // ─── 玩家管理 ────────────────────────────────────── + + /** 踢出玩家 */ + public boolean kickPlayer(String playerName, String reason) { + Player player = Bukkit.getServer().getPlayer(playerName); + if (player == null) { + logger.warn("Cannot kick {}: player not online", playerName); + return false; + } + player.kick(net.kyori.adventure.text.Component.text(reason)); + logger.info("Player {} kicked: {}", playerName, reason); + return true; + } + + /** 封禁玩家名 */ + public boolean banPlayer(String playerName, String reason) { + if (bannedPlayers.contains(playerName)) { + return true; // 已封禁 + } + // 使用 Bukkit BanList API 进行持久化封禁 + OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName); + if (target != null) { + Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME) + .addBan(playerName, reason, null, "Aluer ServerGuard"); + bannedPlayers.add(playerName); + // 如果在线则踢出 + kickPlayer(playerName, reason); + logger.info("Player {} banned: {}", playerName, reason); + return true; + } + return false; + } + + /** 解除玩家封禁 */ + public boolean unbanPlayer(String playerName) { + Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME).pardon(playerName); + bannedPlayers.remove(playerName); + logger.info("Player {} unbanned", playerName); + return true; + } + + /** 封禁 IP */ + public boolean banIP(String ip, String reason) { + if (bannedIPs.contains(ip)) { + return true; + } + try { + Bukkit.getServer().getBanList(org.bukkit.BanList.Type.IP) + .addBan(ip, reason, null, "Aluer ServerGuard"); + bannedIPs.add(ip); + // 踢出该 IP 的所有在线玩家 + for (Player player : Bukkit.getServer().getOnlinePlayers()) { + String playerIP = player.getAddress() != null && player.getAddress().getAddress() != null + ? player.getAddress().getAddress().getHostAddress() + : ""; + if (ip.equals(playerIP)) { + player.kick(net.kyori.adventure.text.Component.text(reason)); + } + } + logger.info("IP {} banned: {}", ip, reason); + return true; + } catch (Exception e) { + logger.error("Failed to ban IP {}: {}", ip, e.getMessage()); + return false; + } + } + + /** 解除 IP 封禁 */ + public boolean unbanIP(String ip) { + Bukkit.getServer().getBanList(org.bukkit.BanList.Type.IP).pardon(ip); + bannedIPs.remove(ip); + logger.info("IP {} unbanned", ip); + return true; + } + + /** 将玩家加入白名单 */ + public boolean addToWhitelist(String playerName) { + OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName); + if (target != null) { + target.setWhitelisted(true); + logger.info("Player {} added to whitelist", playerName); + return true; + } + return false; + } + + /** 从白名单移除玩家 */ + public boolean removeFromWhitelist(String playerName) { + OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName); + if (target != null) { + target.setWhitelisted(false); + return true; + } + return false; + } + + /** 启用白名单模式 */ + public boolean enableWhitelist() { + Bukkit.getServer().setWhitelist(true); + logger.warn("WHITELIST ENABLED — only whitelisted players can join"); + return true; + } + + /** 禁用白名单模式 */ + public boolean disableWhitelist() { + Bukkit.getServer().setWhitelist(false); + logger.info("Whitelist disabled"); + return true; + } + + /** 查询白名单状态 */ + public boolean isWhitelistEnabled() { + return Bukkit.getServer().hasWhitelist(); + } + + // ─── 服务器管理 ───────────────────────────────────── + + /** 保存所有世界 */ + public boolean saveAllWorlds() { + executeCommand("save-all"); + return true; + } + + /** 广播消息给全服玩家 */ + public void broadcast(String message) { + Bukkit.getServer().broadcast( + net.kyori.adventure.text.Component.text(message)); + } + + /** 清理掉落物 */ + public int clearDroppedItems() { + int removed = 0; + for (org.bukkit.World world : Bukkit.getServer().getWorlds()) { + for (org.bukkit.entity.Entity entity : world.getEntities()) { + if (entity instanceof org.bukkit.entity.Item) { + entity.remove(); + removed++; + } + } + } + if (removed > 0) { + logger.info("Cleared {} dropped items across all worlds", removed); + } + return removed; + } + + /** 清理实体(按类型) */ + public int clearEntities(Class entityType) { + int removed = 0; + for (org.bukkit.World world : Bukkit.getServer().getWorlds()) { + for (org.bukkit.entity.Entity entity : world.getEntities()) { + if (entityType.isInstance(entity) && !(entity instanceof Player)) { + entity.remove(); + removed++; + } + } + } + return removed; + } + + /** 设置生物生成速率 */ + public boolean setSpawnRate(int rate) { + for (org.bukkit.World world : Bukkit.getServer().getWorlds()) { + world.setMonsterSpawnLimit(rate); + world.setAnimalSpawnLimit(rate); + } + return true; + } + + /** 获取服务器在线玩家对象列表 */ + public Collection getOnlinePlayers() { + return Bukkit.getServer().getOnlinePlayers(); + } + + /** 按 IP 封禁范围(用于隔离攻击源) */ + public boolean quarantineIP(String ip) { + return banIP(ip, "Aluer Kernel — 安全隔离"); + } + + /** 获取已封禁 IP 列表 */ + public Set getBannedIPs() { + return Collections.unmodifiableSet(bannedIPs); + } +} diff --git a/src/main/java/com/aluer/plugin/listener/BlockEventListener.java b/src/main/java/com/aluer/plugin/listener/BlockEventListener.java new file mode 100644 index 0000000..d3fe2b2 --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/BlockEventListener.java @@ -0,0 +1,153 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.SignChangeEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 方块事件监听器 — 处理方块破坏、放置、告示牌修改 + * + * 实时检测: + * - Nuker(炸图)— 短时间内破坏大量方块 + * - Scaffold(搭桥)— 脚下方块快速连续放置模式 + * - SignExploit(告示牌漏洞)— 异常大小的 NBT 标签内容 + * - Grief(破坏)— 查找特定贵重方块的大规模破坏模式 + */ +public class BlockEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(BlockEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** Nuker 检测窗口(秒内破坏方块数) */ + private static final int NUKER_THRESHOLD = 25; + + /** Scaffold 检测窗口(秒内脚下方块放置数) */ + private static final int SCAFFOLD_THRESHOLD = 8; + + /** 贵重方块列表(Grief 检测目标) */ + private static final Material[] VALUABLE_BLOCKS = { + Material.DIAMOND_BLOCK, Material.GOLD_BLOCK, Material.IRON_BLOCK, + Material.EMERALD_BLOCK, Material.NETHERITE_BLOCK, Material.BEACON, + Material.CHEST, Material.ENDER_CHEST, Material.ENCHANTING_TABLE, + Material.ANVIL, Material.DAMAGED_ANVIL, Material.CHIPPED_ANVIL + }; + + public BlockEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + if (bridge == null) return; + Player player = event.getPlayer(); + PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); + if (snap == null) return; + + Block block = event.getBlock(); + String posKey = String.format("%d,%d,%d", + block.getX(), block.getY(), block.getZ()); + + snap.recentBlockBreaks.add(posKey); + while (snap.recentBlockBreaks.size() > 50) { + snap.recentBlockBreaks.removeFirst(); + } + + bridge.incrementEvent("block.break"); + + // —— Nuker 检测 —— + if (snap.recentBlockBreaks.size() >= NUKER_THRESHOLD) { + bridge.alert(AlertType.SECURITY_NUKER, + String.format("玩家 %s 短时间内破坏 %d 个方块,疑似 Nuker", + player.getName(), snap.recentBlockBreaks.size()), + Math.min(0.95, snap.recentBlockBreaks.size() / 40.0), + player.getName()); + } + + // —— Grief 检测:大量破坏贵重方块 —— + Material type = block.getType(); + for (Material valuable : VALUABLE_BLOCKS) { + if (type == valuable) { + bridge.alert(AlertType.SECURITY_GRIEF, + String.format("玩家 %s 破坏贵重方块 %s 位于 %s", + player.getName(), type.name(), posKey), + 0.6, player.getName()); + break; + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + if (bridge == null) return; + Player player = event.getPlayer(); + PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); + if (snap == null) return; + + Block block = event.getBlock(); + String posKey = String.format("%d,%d,%d", + block.getX(), block.getY(), block.getZ()); + + snap.recentBlockPlaces.add(posKey); + while (snap.recentBlockPlaces.size() > 50) { + snap.recentBlockPlaces.removeFirst(); + } + + bridge.incrementEvent("block.place"); + + // —— Scaffold 检测:脚下方块快速放置 —— + // 检查是否在玩家脚下方块附近放置 + double playerY = snap.y; + if (Math.abs(block.getY() - (playerY - 1)) < 1.5) { + // 脚下方块 + int footPlaces = 0; + for (String p : snap.recentBlockPlaces) { + String[] parts = p.split(","); + if (parts.length == 3) { + try { + double by = Double.parseDouble(parts[1]); + if (Math.abs(by - (playerY - 1)) < 1.5) footPlaces++; + } catch (NumberFormatException ignored) {} + } + } + if (footPlaces >= SCAFFOLD_THRESHOLD) { + bridge.alert(AlertType.SECURITY_SCAFFOLD, + String.format("玩家 %s 疑似 Scaffold:快速在脚下方块放置", player.getName()), + 0.75, player.getName()); + } + } + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onSignChange(SignChangeEvent event) { + if (bridge == null) return; + Player player = event.getPlayer(); + + // —— SignExploit 检测 —— + // 检测异常长的告示牌文本(可能导致崩溃的 NBT 攻击) + for (int i = 0; i < 4; i++) { + String line = event.getLine(i); + if (line != null && line.length() > 256) { + bridge.alert(AlertType.SECURITY_SIGN_EXPLOIT, + String.format("玩家 %s 告示牌文本异常长(%d字符),疑似 SignExploit", + player.getName(), line.length()), + 0.9, player.getName()); + event.setCancelled(true); + return; + } + } + } +} diff --git a/src/main/java/com/aluer/plugin/listener/ChatEventListener.java b/src/main/java/com/aluer/plugin/listener/ChatEventListener.java new file mode 100644 index 0000000..a6414dc --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/ChatEventListener.java @@ -0,0 +1,98 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.regex.Pattern; + +/** + * 聊天事件监听器 — 处理玩家聊天消息 + * + * 实时检测: + * - ChatFlood(聊天刷屏)— 短时间内大量消息 + * - Advertisement(广告)— IP/域名模式匹配 + * - PhishingLink(钓鱼链接)— 可疑 URL 检测 + * - Profanity(不雅用语)— 关键词过滤 + * - Spam(重复消息)— 相同内容重复发送 + */ +public class ChatEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(ChatEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** IP 地址正则 */ + private static final Pattern IP_PATTERN = Pattern.compile( + "(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d{1,5})?"); + + /** URL 正则(含常见短链接服务) */ + private static final Pattern URL_PATTERN = Pattern.compile( + "https?://[^\\s]+|bit\\.ly/[^\\s]+|tinyurl\\.com/[^\\s]+|discord\\.gg/[^\\s]+", + Pattern.CASE_INSENSITIVE); + + /** 聊天刷屏阈值(秒内消息数) */ + private static final int SPAM_THRESHOLD = 5; + private static final long SPAM_WINDOW_MS = 10_000; + + public ChatEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onAsyncPlayerChat(AsyncPlayerChatEvent event) { + if (bridge == null) return; + String message = event.getMessage(); + String playerName = event.getPlayer().getName(); + PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId()); + + // —— 聊天刷屏检测 —— + if (snap != null) { + snap.recentMessages.add(message); + while (snap.recentMessages.size() > 20) { + snap.recentMessages.removeFirst(); + } + + long now = System.currentTimeMillis(); + long recentCount = snap.recentMessages.stream() + .filter(m -> true) // 所有消息都在时间窗口内(Deque 中保留最近 20 条) + .count(); + + if (recentCount >= SPAM_THRESHOLD) { + bridge.alert(AlertType.CHAT_FLOOD, + String.format("玩家 %s 聊天刷屏:%d 条消息", playerName, recentCount), + 0.7, playerName); + event.setCancelled(true); + return; + } + } + + // —— 广告/IP 检测 —— + if (IP_PATTERN.matcher(message).find()) { + bridge.alert(AlertType.CHAT_ADVERTISEMENT, + String.format("玩家 %s 发送疑似IP地址:%s", playerName, message), + 0.8, playerName); + event.setCancelled(true); + return; + } + + // —— 钓鱼链接检测 —— + if (URL_PATTERN.matcher(message).find()) { + bridge.alert(AlertType.CHAT_PHISHING, + String.format("玩家 %s 发送外部链接:%s", playerName, message), + 0.75, playerName); + event.setCancelled(true); + return; + } + + bridge.incrementEvent("chat.message"); + } +} diff --git a/src/main/java/com/aluer/plugin/listener/CombatEventListener.java b/src/main/java/com/aluer/plugin/listener/CombatEventListener.java new file mode 100644 index 0000000..3bef470 --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/CombatEventListener.java @@ -0,0 +1,112 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.util.Vector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 战斗事件监听器 — 处理玩家攻击、实体伤害、死亡事件 + * + * 实时检测: + * - KillAura(杀戮光环)— 多目标快速切换 + 攻击角度一致性 + * - Reach(攻击距离扩展)— 攻击距离超过 3.0 方块 + * - AutoClicker(自动连点)— 异常攻击频率 + * - ChestSteal — 容器交互跟踪由 InventoryEventListener 处理 + */ +public class CombatEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(CombatEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** 正常最大攻击距离(方块) */ + private static final double MAX_ATTACK_DISTANCE = 3.0; + + /** 攻击距离扩展容忍度 */ + private static final double REACH_TOLERANCE = 3.3; + + public CombatEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityDamageByEntity(EntityDamageByEntityEvent event) { + if (bridge == null) return; + + // 仅处理玩家攻击 + if (!(event.getDamager() instanceof Player attacker)) return; + Entity victim = event.getEntity(); + + PlayerSnapshot attackerSnap = bridge.getPlayer(attacker.getUniqueId()); + if (attackerSnap == null) return; + + // —— 记录攻击目标(KillAura 检测用) —— + attackerSnap.recordTarget(victim.getName()); + + // —— KillAura 检测:多目标快速切换 —— + int recentTargets = attackerSnap.getRecentTargetCount(); + if (recentTargets > 4) { + bridge.alert(AlertType.SECURITY_KILL_AURA, + String.format("玩家 %s 在3秒内攻击了 %d 个不同目标", attacker.getName(), recentTargets), + Math.min(0.95, recentTargets * 0.2), attacker.getName()); + } + + // —— Reach 检测:攻击距离 —— + double distance = attacker.getLocation().distance(victim.getLocation()); + attackerSnap.recordAttackDistance(distance); + if (distance > REACH_TOLERANCE) { + bridge.alert(AlertType.SECURITY_REACH, + String.format("玩家 %s 攻击距离异常:%.2f 方块(最大 %.2f)", + attacker.getName(), distance, MAX_ATTACK_DISTANCE), + Math.min(0.95, (distance - MAX_ATTACK_DISTANCE) * 0.5 + 0.5), + attacker.getName()); + } + + // —— KillAura:攻击角度一致性 —— + Vector attackerDir = attacker.getLocation().getDirection(); + double angle = Math.atan2( + victim.getLocation().getZ() - attacker.getLocation().getZ(), + victim.getLocation().getX() - attacker.getLocation().getX() + ) * 180.0 / Math.PI; + attackerSnap.recordAttackAngle(angle); + if (attackerSnap.hasSuspiciousAngleConsistency()) { + bridge.alert(AlertType.SECURITY_KILL_AURA, + String.format("玩家 %s 攻击角度高度一致,疑似 Aimbot", attacker.getName()), + 0.85, attacker.getName()); + } + + // —— AutoClicker 检测:攻击频率异常 —— + long now = System.currentTimeMillis(); + long recentAttacks = attackerSnap.recentTargets.values().stream() + .filter(t -> t > now - 1000) + .count(); + if (recentAttacks > 20) { // 每秒超过20次攻击(正常CPS上限约14) + bridge.alert(AlertType.SECURITY_AUTO_CLICKER, + String.format("玩家 %s 攻击频率异常:每秒 %d 次", attacker.getName(), recentAttacks), + 0.8, attacker.getName()); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onEntityDeath(EntityDeathEvent event) { + if (bridge == null) return; + Entity entity = event.getEntity(); + Player killer = event.getEntity().getKiller(); + + if (killer != null && entity instanceof Player victim) { + bridge.incrementEvent("combat.player_kill"); + } + } +} diff --git a/src/main/java/com/aluer/plugin/listener/CommandEventListener.java b/src/main/java/com/aluer/plugin/listener/CommandEventListener.java new file mode 100644 index 0000000..0e083aa --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/CommandEventListener.java @@ -0,0 +1,88 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 命令事件监听器 — 监控所有玩家和后台命令执行 + * + * 实时检测: + * - CommandAbuse(命令滥用)— OP 权限的敏感命令使用 + * - 插件后门命令 — 异常插件注册的危险命令 + * - 命令注入 — 包含 shell 特殊字符的命令 + */ +public class CommandEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(CommandEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** 高危命令前缀列表(需要告警监控) */ + private static final String[] SENSITIVE_COMMANDS = { + "op ", "deop ", "ban ", "pardon ", "whitelist ", "stop", "restart", + "reload", "plugins", "version", "about", "icanhasbukkit", + "save-all", "save-on", "save-off", "difficulty", "defaultgamemode", + "seed", "worldborder", "spreadplayers", "fill", "clone", + "execute", "function", "datapack", "gamerule", "kill", + "summon", "give", "clear", "enchant", "effect", + "tp ", "teleport ", "gamemode ", "kick " + }; + + public CommandEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerCommand(PlayerCommandPreprocessEvent event) { + if (bridge == null) return; + String command = event.getMessage().toLowerCase().trim(); + String playerName = event.getPlayer().getName(); + PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId()); + + bridge.incrementEvent("command.player"); + + // 记录最近命令(用于滥用检测) + if (snap != null) { + snap.recentCommands.add(command); + while (snap.recentCommands.size() > 10) { + snap.recentCommands.removeFirst(); + } + } + + // —— 敏感命令监控 —— + for (String sensitive : SENSITIVE_COMMANDS) { + if (command.startsWith("/" + sensitive) || command.startsWith("minecraft:" + sensitive)) { + bridge.alert(AlertType.COMMAND_ABUSE, + String.format("玩家 %s 执行敏感命令:%s", playerName, event.getMessage()), + 0.6, playerName); + break; + } + } + + // —— 命令注入检测 —— + if (command.contains("${") || command.contains("`") || + command.contains("&&") || command.contains("||") || + command.contains(";") && command.contains("rm ")) { + bridge.alert(AlertType.COMMAND_ABUSE, + String.format("玩家 %s 命令疑似注入:%s", playerName, event.getMessage()), + 0.9, playerName); + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onServerCommand(ServerCommandEvent event) { + if (bridge == null) return; + bridge.incrementEvent("command.server"); + } +} diff --git a/src/main/java/com/aluer/plugin/listener/EntityEventListener.java b/src/main/java/com/aluer/plugin/listener/EntityEventListener.java new file mode 100644 index 0000000..4dad65f --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/EntityEventListener.java @@ -0,0 +1,83 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.PlayerFishEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 实体事件监听器 — 处理实体生成、拾取、交互 + * + * 实时检测: + * - AutoFish(自动钓鱼)— 异常快速的钓鱼收杆时机 + * - 实体生成速率异常(可能为故意的服务器压力测试) + */ +public class EntityEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(EntityEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + public EntityEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntitySpawn(EntitySpawnEvent event) { + if (bridge == null) return; + bridge.incrementEvent("entity.spawn"); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerFish(PlayerFishEvent event) { + if (bridge == null) return; + Player player = event.getPlayer(); + + // —— AutoFish 检测 —— + // 正常钓鱼需要玩家注意力响应,AutoFish hack 会在鱼咬钩瞬间自动收杆 + if (event.getState() == PlayerFishEvent.State.CAUGHT_FISH + || event.getState() == PlayerFishEvent.State.CAUGHT_ENTITY) { + + bridge.incrementEvent("fish.caught"); + + // 记录钓鱼收杆时间,如果连续快速收杆则标记 + var snap = bridge.getPlayer(player.getUniqueId()); + if (snap != null) { + long now = System.currentTimeMillis(); + long recentCatches = snap.recentTargets.values().stream() + .filter(t -> t > now - 30_000) // 30秒内 + .count(); + if (recentCatches > 5) { // 30秒内收杆超过5次(正常钓鱼约15-60秒一次) + bridge.alert(AlertType.SECURITY_AUTO_FISH, + String.format("玩家 %s 30秒内钓鱼收杆 %d 次,疑似 AutoFish", + player.getName(), recentCatches), + 0.7, player.getName()); + } + // 复用 recentTargets 的 key 为 "fish" 来记录钓鱼时间 + snap.recentTargets.put("fish:" + System.nanoTime(), now); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityPickupItem(EntityPickupItemEvent event) { + if (bridge == null) return; + if (event.getEntity() instanceof Player) { + bridge.incrementEvent("entity.pickup"); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onEntityExplode(EntityExplodeEvent event) { + if (bridge == null) return; + bridge.incrementEvent("entity.explode"); + } +} diff --git a/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java b/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java new file mode 100644 index 0000000..4d00fc7 --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java @@ -0,0 +1,116 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; +import org.bukkit.block.Container; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.*; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 物品栏事件监听器 — 处理容器交互、物品移动 + * + * 实时检测: + * - ChestSteal(快速偷箱)— 短时间内从容器取出大量物品 + * - InventoryManipulation(物品栏操控)— 异常快速的物品移动模式 + * - Dupe(物品复制)— 异常物品数量变化 + */ +public class InventoryEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(InventoryEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** 单次 Click 事件中物品数量变化的阈值(超过视为可疑) */ + private static final int ITEM_CHANGE_THRESHOLD = 64; + + public InventoryEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + if (bridge == null) return; + if (!(event.getWhoClicked() instanceof Player player)) return; + + Inventory inv = event.getInventory(); + InventoryType type = inv.getType(); + PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); + if (snap == null) return; + + bridge.incrementEvent("inventory.click"); + + // —— ChestSteal 检测 —— + // 检测玩家在短时间内从他人容器中快速取走物品 + if (type == InventoryType.CHEST || type == InventoryType.BARREL + || type == InventoryType.SHULKER_BOX || type == InventoryType.ENDER_CHEST) { + + ItemStack current = event.getCurrentItem(); + ItemStack cursor = event.getCursor(); + + // 快速移动(Shift-Click)整组物品 + if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY && current != null) { + int movedAmount = current.getAmount(); + if (movedAmount >= ITEM_CHANGE_THRESHOLD / 2) { + bridge.alert(AlertType.SECURITY_CHEST_STEAL, + String.format("玩家 %s 从容器快速取走 %d 个物品", + player.getName(), movedAmount), + 0.5, player.getName()); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onInventoryOpen(InventoryOpenEvent event) { + if (bridge == null) return; + if (!(event.getPlayer() instanceof Player player)) return; + + bridge.incrementEvent("inventory.open"); + + // 记录容器打开以辅助 ChestSteal 上下文判断 + Inventory inv = event.getInventory(); + if (inv.getHolder() instanceof Container container) { + PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); + if (snap != null) { + // 记录容器位置用于后续分析 + String locKey = String.format("container:%d,%d,%d", + container.getLocation().getBlockX(), + container.getLocation().getBlockY(), + container.getLocation().getBlockZ()); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onInventoryClose(InventoryCloseEvent event) { + if (bridge == null) return; + if (!(event.getPlayer() instanceof Player)) return; + bridge.incrementEvent("inventory.close"); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryDrag(InventoryDragEvent event) { + if (bridge == null) return; + if (!(event.getWhoClicked() instanceof Player player)) return; + bridge.incrementEvent("inventory.drag"); + + // 大量物品拖动(可能为 InventoryManipulation hack) + int totalSlots = event.getInventorySlots().size(); + if (totalSlots > 16) { + bridge.alert(AlertType.SECURITY_INVENTORY_MANIPULATION, + String.format("玩家 %s 一次拖动覆盖 %d 个物品栏位,疑似 InventoryManipulation", + player.getName(), totalSlots), + 0.65, player.getName()); + } + } +} diff --git a/src/main/java/com/aluer/plugin/listener/PacketEventListener.java b/src/main/java/com/aluer/plugin/listener/PacketEventListener.java new file mode 100644 index 0000000..f16f180 --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/PacketEventListener.java @@ -0,0 +1,59 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 包级事件监听器 — 利用 Paper API 对网络包进行细粒度监控 + * + * Paper 特有 API,非 Paper 服务端会自动跳过注册。 + * + * 实时检测: + * - PacketFlood(包洪水)— 单个玩家发送异常数量的包 + * - TabCompleteCrash — 异常大的 TabComplete 请求 + * - BookBan — 超大书内容导致的崩溃包 + * - ResourcePackExploit — 恶意资源包下载尝试 + */ +public class PacketEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(PacketEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** 每秒最大允许的包速率(单玩家) */ + private static final int MAX_PACKETS_PER_SECOND = 200; + + public PacketEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + /** + * 监听 Paper 的包预接收事件(需 Paper 1.20.6+) + * 使用反射调用避免编译时硬依赖 Paper 特定 API 类 + */ + @EventHandler(priority = EventPriority.LOWEST) + public void onPacketReceive(com.destroystokyo.paper.event.player.PlayerHandshakeEvent event) { + if (bridge == null) return; + // 握手阶段即可获取连接来源 + bridge.incrementEvent("packet.handshake"); + } + + /** + * 通用的包事件监听 — 使用 Paper 1.21+ 的 AsyncPacketReceiveEvent + * 注:此类在 paper-api 1.21 中可用,但具体 API 可能因版本而异 + */ + // @EventHandler 注释留空,待 Paper 版本确认后启用 + // 当前通过 DataBridge.recordPacket() 提供包速率追踪入口 + public void onAsyncPacket(Object event) { + if (bridge == null) return; + bridge.recordPacket(); + bridge.incrementEvent("packet.total"); + } +} diff --git a/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java b/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java new file mode 100644 index 0000000..ca27da8 --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java @@ -0,0 +1,184 @@ +package com.aluer.plugin.listener; + +import com.aluer.model.AlertType; +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 玩家事件监听器 — 处理登录、退出、移动、传送、死亡等核心玩家事件 + * + * 实时检测: + * - Speed(移动速度异常)— 水平速度超过 0.65 blocks/tick 则标记 + * - Fly(非法飞行)— 非创造/旁观模式下,垂直位移模式异常 + * - Jesus(水行)— 连续在水中但 y 坐标不下降 + * - NoFall(无摔伤)— 从高处落地无伤害 + */ +public class PlayerEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(PlayerEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** 最大允许水平速度(blocks/tick),超过此值视为 Speed Hack */ + private static final double MAX_HORIZONTAL_SPEED = 0.65; + + /** 可疑移动的告警置信度阈值 */ + private static final double SUSPICIOUS_CONFIDENCE = 0.75; + + public PlayerEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + // ─── 登录/退出 ────────────────────────────────────── + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + if (bridge == null) return; + Player player = event.getPlayer(); + bridge.registerPlayer(player); + bridge.incrementEvent("player.join"); + + // 检查同 IP 多账号 + String ip = player.getAddress() != null && player.getAddress().getAddress() != null + ? player.getAddress().getAddress().getHostAddress() + : "unknown"; + var sameIP = bridge.getPlayersByIP(ip); + if (sameIP.size() > 2) { + bridge.alert(AlertType.SECURITY_ALT_ACCOUNT, + String.format("玩家 %s 使用IP %s 登录,该IP已有 %d 个在线账号", + player.getName(), ip, sameIP.size()), + 0.7, player.getName()); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + if (bridge == null) return; + bridge.removePlayer(event.getPlayer().getUniqueId()); + bridge.incrementEvent("player.quit"); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerKick(PlayerKickEvent event) { + if (bridge == null) return; + bridge.removePlayer(event.getPlayer().getUniqueId()); + bridge.incrementEvent("player.kick"); + } + + // ─── 移动检测 ────────────────────────────────────── + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerMove(PlayerMoveEvent event) { + if (bridge == null) return; + Player player = event.getPlayer(); + PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); + if (snap == null) return; + + Location from = event.getFrom(); + Location to = event.getTo(); + if (to == null) return; + + // 仅在位置实际改变时更新 + if (from.getX() == to.getX() && from.getY() == to.getY() && from.getZ() == to.getZ()) return; + + // 更新快照位置 + snap.updatePosition(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); + + // 计算水平移动距离 + double dx = to.getX() - from.getX(); + double dz = to.getZ() - from.getZ(); + double horizontalDist = Math.sqrt(dx * dx + dz * dz); + double dy = to.getY() - from.getY(); + + snap.totalDistanceMoved += horizontalDist; + + // —— Speed 检测 —— + // 排除创造/旁观模式、鞘翅飞行、骑乘状态 + if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL + && !player.isGliding() + && !player.isInsideVehicle() + && !player.isFlying()) { + + if (horizontalDist > MAX_HORIZONTAL_SPEED) { + bridge.alert(AlertType.SECURITY_SPEED, + String.format("玩家 %s 移动速度异常:水平 %.3f blocks/tick(阈值 %.2f)", + player.getName(), horizontalDist, MAX_HORIZONTAL_SPEED), + Math.min(0.95, horizontalDist / MAX_HORIZONTAL_SPEED * 0.6), + player.getName()); + } + } + + // —— Fly 检测 —— + // 非飞行玩家在空中时持续 y 坐标下降过慢 + if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL + && !player.isFlying() + && !player.isGliding() + && !player.isInWater() + && !player.isInsideVehicle()) { + + // 如果玩家在空中(脚下无方块)且 y 不下降反而上升 + boolean onGround = ((org.bukkit.entity.LivingEntity) player).isOnGround(); + if (!onGround && dy > 0.05 && horizontalDist < 0.1) { + bridge.alert(AlertType.SECURITY_FLY, + String.format("玩家 %s 疑似飞行:空中上升 dy=%.3f", player.getName(), dy), + SUSPICIOUS_CONFIDENCE, player.getName()); + } + } + + // —— Jesus(水行)检测 —— + if (player.isInWater() && player.getGameMode() == org.bukkit.GameMode.SURVIVAL + && !player.isFlying() && !player.isSwimming()) { + // 在水中但 y 坐标超过水面特征线 + if (dy > -0.01 && horizontalDist > 0.3) { + bridge.alert(AlertType.SECURITY_JESUS, + String.format("玩家 %s 疑似水行:水中水平移动 %.3f blocks", player.getName(), horizontalDist), + 0.65, player.getName()); + } + } + } + + // ─── 传送跟踪 ────────────────────────────────────── + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerTeleport(PlayerTeleportEvent event) { + if (bridge == null) return; + PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId()); + if (snap != null && event.getTo() != null) { + snap.updatePosition(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(), + event.getTo().getYaw(), event.getTo().getPitch()); + } + } + + // ─── 摔伤检测(NoFall) ───────────────────────────── + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerDamage(EntityDamageEvent event) { + if (bridge == null) return; + if (!(event.getEntity() instanceof Player player)) return; + if (event.getCause() != EntityDamageEvent.DamageCause.FALL) return; + + PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); + if (snap == null) return; + + double fallDistance = player.getFallDistance(); + // 正常情况:掉落 > 3 方块高度应有伤害 + // NoFall hack 会让服务端认为掉落距离为 0 + if (fallDistance > 3.5 && event.getDamage() < 1.0) { + bridge.alert(AlertType.SECURITY_NOFALL, + String.format("玩家 %s 疑似 NoFall:掉落 %.1f 方块但伤害仅 %.1f", + player.getName(), fallDistance, event.getDamage()), + 0.8, player.getName()); + } + } +} diff --git a/src/main/java/com/aluer/plugin/listener/WorldEventListener.java b/src/main/java/com/aluer/plugin/listener/WorldEventListener.java new file mode 100644 index 0000000..3e08a67 --- /dev/null +++ b/src/main/java/com/aluer/plugin/listener/WorldEventListener.java @@ -0,0 +1,58 @@ +package com.aluer.plugin.listener; + +import com.aluer.plugin.AluerPlugin; +import com.aluer.plugin.bridge.DataBridge; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.event.world.WorldSaveEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 世界事件监听器 — 处理区块加载/卸载、世界保存 + * + * 主要用于性能监控: + * - 区块加载速率(异常高可能表明 Xray 或 Baritone 矿透机器人) + * - 世界保存耗时跟踪 + */ +public class WorldEventListener implements Listener { + private static final Logger logger = LoggerFactory.getLogger(WorldEventListener.class); + + private final AluerPlugin plugin; + private final DataBridge bridge; + + /** 区块加载速率告警阈值(每秒区块数),Baritone/Xray 会快速加载大量区块 */ + private static final int CHUNK_LOAD_RATE_WARNING = 30; + + public WorldEventListener(AluerPlugin plugin, DataBridge bridge) { + this.plugin = plugin; + this.bridge = bridge; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onChunkLoad(ChunkLoadEvent event) { + if (bridge == null) return; + long count = bridge.incrementEvent("world.chunk_load"); + + // 高频区块加载可能表明 Baritone 矿透或 Xray 探索 + if (count > CHUNK_LOAD_RATE_WARNING) { + // 每秒重置计数器(由 DataBridge 的调度处理) + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onChunkUnload(ChunkUnloadEvent event) { + if (bridge == null) return; + bridge.incrementEvent("world.chunk_unload"); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldSave(WorldSaveEvent event) { + if (bridge == null) return; + bridge.incrementEvent("world.save"); + logger.debug("World saved: {}", event.getWorld().getName()); + } +} diff --git a/src/main/java/com/aluer/service/AutoExecutor.java b/src/main/java/com/aluer/service/AutoExecutor.java index 04a2650..66baeaa 100644 --- a/src/main/java/com/aluer/service/AutoExecutor.java +++ b/src/main/java/com/aluer/service/AutoExecutor.java @@ -4,8 +4,10 @@ import com.aluer.ai.DeepSeekClient; import com.aluer.config.ServerGuardConfig; import com.aluer.model.AlertEvent; import com.aluer.model.AlertType; +import com.aluer.plugin.bridge.InternalCommandExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; @@ -16,16 +18,20 @@ import java.util.regex.Pattern; @Component public class AutoExecutor { private static final Logger logger = LoggerFactory.getLogger(AutoExecutor.class); - + private final ServerGuardConfig config; private final RconClient rconClient; private final DeepSeekClient deepSeekClient; private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); private final ConcurrentHashMap recentActions = new ConcurrentHashMap<>(); - + private static final Pattern IP_PATTERN = Pattern.compile("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"); private static final long ACTION_COOLDOWN_MS = 60000; + /** 插件模式下的内部命令执行器(external 模式下为 null) */ + @Autowired(required = false) + private InternalCommandExecutor internalCommandExecutor; + public AutoExecutor(ServerGuardConfig config, RconClient rconClient, DeepSeekClient deepSeekClient) { this.config = config; this.rconClient = rconClient; @@ -86,13 +92,15 @@ public class AutoExecutor { logger.info("[AUTO-EXEC] Ban disabled in config"); return; } - + String ip = extractIpFromAlert(alert); - + if (ip != null) { String banKey = "ban-ip:" + ip; if (!isActionCooldown(banKey)) { - boolean success = rconClient.banIp(ip); + boolean success = isPluginMode() + ? internalCommandExecutor.banIP(ip, "AI 自动封禁 — " + alert.getType().getTitle()) + : rconClient.banIp(ip); logger.info("[AUTO-EXEC] Ban IP {} - {}", ip, success ? "SUCCESS" : "FAILED"); recordAction(banKey); } @@ -101,7 +109,9 @@ public class AutoExecutor { if (player != null) { String banKey = "ban-player:" + player; if (!isActionCooldown(banKey)) { - boolean success = rconClient.banPlayer(player); + boolean success = isPluginMode() + ? internalCommandExecutor.banPlayer(player, "AI 自动封禁 — " + alert.getType().getTitle()) + : rconClient.banPlayer(player); logger.info("[AUTO-EXEC] Ban player {} - {}", player, success ? "SUCCESS" : "FAILED"); recordAction(banKey); } @@ -113,12 +123,14 @@ public class AutoExecutor { if (!config.getAi().getDeepseek().getAutoExecute().isKickPlayer()) { return; } - + String player = extractPlayerFromAlert(alert); if (player != null) { String kickKey = "kick:" + player; if (!isActionCooldown(kickKey)) { - boolean success = rconClient.kickPlayer(player, "Auto-kick by AI security system"); + boolean success = isPluginMode() + ? internalCommandExecutor.kickPlayer(player, "AI 安全系统自动踢出") + : rconClient.kickPlayer(player, "Auto-kick by AI security system"); logger.info("[AUTO-EXEC] Kick player {} - {}", player, success ? "SUCCESS" : "FAILED"); recordAction(kickKey); } @@ -129,14 +141,18 @@ public class AutoExecutor { if (!config.getAi().getDeepseek().getAutoExecute().isClearLag()) { return; } - + String clearKey = "clear-lag"; if (!isActionCooldown(clearKey)) { - boolean success = rconClient.clearLag(); - logger.info("[AUTO-EXEC] Clear lag - {}", success ? "SUCCESS" : "FAILED"); + if (isPluginMode()) { + int removed = internalCommandExecutor.clearDroppedItems(); + logger.info("[AUTO-EXEC] Clear lag - removed {} dropped items via Bukkit API", removed); + } else { + boolean success = rconClient.clearLag(); + logger.info("[AUTO-EXEC] Clear lag - {}", success ? "SUCCESS" : "FAILED"); + rconClient.killAllMobs(); + } recordAction(clearKey); - - rconClient.killAllMobs(); } } @@ -144,10 +160,14 @@ public class AutoExecutor { if (!config.getAi().getDeepseek().getAutoExecute().isSetSpawnRate()) { return; } - + String spawnKey = "spawn-control"; if (!isActionCooldown(spawnKey)) { - rconClient.setSpawnRate(5); + if (isPluginMode()) { + internalCommandExecutor.setSpawnRate(5); + } else { + rconClient.setSpawnRate(5); + } logger.info("[AUTO-EXEC] Reduced spawn rate to prevent lag"); recordAction(spawnKey); } @@ -157,14 +177,22 @@ public class AutoExecutor { if (!config.getAi().getDeepseek().getAutoExecute().isWhitelist()) { return; } - + String whitelistKey = "whitelist"; if (!isActionCooldown(whitelistKey)) { if (action.contains("enable") || action.contains("开启") || action.contains("启用")) { - rconClient.enableWhitelist(); + if (isPluginMode()) { + internalCommandExecutor.enableWhitelist(); + } else { + rconClient.enableWhitelist(); + } logger.info("[AUTO-EXEC] Enabled whitelist"); } else if (action.contains("disable") || action.contains("关闭")) { - rconClient.disableWhitelist(); + if (isPluginMode()) { + internalCommandExecutor.disableWhitelist(); + } else { + rconClient.disableWhitelist(); + } logger.info("[AUTO-EXEC] Disabled whitelist"); } recordAction(whitelistKey); @@ -176,10 +204,15 @@ public class AutoExecutor { if (!isActionCooldown(restartKey)) { logger.warn("[AUTO-EXEC] AI recommended server restart"); logger.warn("[AUTO-EXEC] Sending alert to admin before restart..."); - - rconClient.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues"); - rconClient.executeCommand("save-all"); - + + if (isPluginMode()) { + internalCommandExecutor.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues"); + internalCommandExecutor.saveAllWorlds(); + } else { + rconClient.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues"); + rconClient.executeCommand("save-all"); + } + recordAction(restartKey); } } @@ -187,7 +220,11 @@ public class AutoExecutor { private void executeWorldBorder() { String borderKey = "world-border"; if (!isActionCooldown(borderKey)) { - rconClient.setWorldBorder(1000); + if (isPluginMode()) { + internalCommandExecutor.executeCommand("worldborder set 1000"); + } else { + rconClient.setWorldBorder(1000); + } logger.info("[AUTO-EXEC] Set world border to reduce entity spawns"); recordAction(borderKey); } @@ -195,13 +232,18 @@ public class AutoExecutor { private String extractIpFromAlert(AlertEvent alert) { String message = alert.getMessage(); - + Matcher matcher = IP_PATTERN.matcher(message); if (matcher.find()) { return matcher.group(); } - - return rconClient.extractIpFromMessage(message); + + return isPluginMode() ? null : rconClient.extractIpFromMessage(message); + } + + /** 当前是否运行在 Paper 插件模式 */ + private boolean isPluginMode() { + return config.isPluginMode() && internalCommandExecutor != null; } private String extractPlayerFromAlert(AlertEvent alert) { @@ -255,13 +297,25 @@ public class AutoExecutor { ExecuteTask task; while ((task = taskQueue.poll()) != null) { try { - switch (task.getType()) { - case BAN_IP -> rconClient.banIp(task.getTarget()); - case BAN_PLAYER -> rconClient.banPlayer(task.getTarget()); - case KICK -> rconClient.kickPlayer(task.getTarget(), task.getReason()); - case CLEAR_LAG -> rconClient.clearLag(); - case RESTART -> rconClient.restartServer(); - default -> logger.warn("Unknown task type: {}", task.getType()); + if (isPluginMode()) { + switch (task.getType()) { + case BAN_IP -> internalCommandExecutor.banIP(task.getTarget(), task.getReason()); + case BAN_PLAYER -> internalCommandExecutor.banPlayer(task.getTarget(), task.getReason()); + case KICK -> internalCommandExecutor.kickPlayer(task.getTarget(), task.getReason()); + case CLEAR_LAG -> internalCommandExecutor.clearDroppedItems(); + case RESTART -> internalCommandExecutor.executeCommand("restart"); + case WHITELIST -> internalCommandExecutor.enableWhitelist(); + default -> logger.warn("Unknown task type: {}", task.getType()); + } + } else { + switch (task.getType()) { + case BAN_IP -> rconClient.banIp(task.getTarget()); + case BAN_PLAYER -> rconClient.banPlayer(task.getTarget()); + case KICK -> rconClient.kickPlayer(task.getTarget(), task.getReason()); + case CLEAR_LAG -> rconClient.clearLag(); + case RESTART -> rconClient.restartServer(); + default -> logger.warn("Unknown task type: {}", task.getType()); + } } } catch (Exception e) { logger.error("Failed to execute task: {}", e.getMessage()); diff --git a/src/main/java/com/aluer/service/ServerGuardService.java b/src/main/java/com/aluer/service/ServerGuardService.java index c404e3a..62dcb44 100644 --- a/src/main/java/com/aluer/service/ServerGuardService.java +++ b/src/main/java/com/aluer/service/ServerGuardService.java @@ -6,8 +6,11 @@ import com.aluer.config.ServerGuardConfig; import com.aluer.monitor.*; import com.aluer.model.AlertEvent; import com.aluer.model.MetricsData; +import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.InternalCommandExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Service; @@ -36,6 +39,14 @@ public class ServerGuardService implements CommandLineRunner { private final AutoExecutor autoExecutor; private final RconClient rconClient; + /** 插件模式下的数据桥接(external 模式下为 null) */ + @Autowired(required = false) + private DataBridge dataBridge; + + /** 插件模式下的内部命令执行器(external 模式下为 null) */ + @Autowired(required = false) + private InternalCommandExecutor internalCommandExecutor; + @Value("${server.port:8080}") private int serverPort; @@ -84,6 +95,20 @@ public class ServerGuardService implements CommandLineRunner { startMonitoring(); + // 插件模式:注册 DataBridge 告警/指标处理器 + if (config.isPluginMode() && dataBridge != null) { + dataBridge.setAlertHandler(this::handleAlert); + dataBridge.setMetricsHandler(metrics -> { + deepSeekClient.addMetrics(metrics); + adaptiveThreshold.addMetric(metrics); + anomalyDetector.addMetric(metrics); + timeSeriesPredictor.addMetric(metrics); + }); + // 启动 DataBridge 包速率定时更新 + scheduler.scheduleAtFixedRate(() -> dataBridge.updatePacketRate(), 1, 1, TimeUnit.SECONDS); + logger.info("DataBridge alert/metrics handlers registered — plugin mode active"); + } + Runtime.getRuntime().addShutdownHook(new Thread(() -> { logger.info("Shutdown signal received, gracefully stopping..."); running.set(false); @@ -107,8 +132,9 @@ public class ServerGuardService implements CommandLineRunner { private void printBanner() { logger.info(""); logger.info("╔══════════════════════════════════════════════════════════════╗"); - logger.info("║ Aluer ServerGuard v1.0.0 ║"); + logger.info("║ Aluer ServerGuard v4.0.0 ║"); logger.info("║ AI-Powered Minecraft Server Protection ║"); + logger.info("║ Mode: " + String.format("%-39s", config.isPluginMode() ? "Paper Plugin (Embedded)" : "External Monitor") + "║"); logger.info("╠══════════════════════════════════════════════════════════════╣"); logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version"))); logger.info("║ Available CPUs: {}{} ║", @@ -210,6 +236,15 @@ public class ServerGuardService implements CommandLineRunner { private void collectAndAnalyzeMetrics() { try { + // 插件模式:从 DataBridge 采集实时指标 + if (config.isPluginMode() && dataBridge != null) { + MetricsData pluginMetrics = dataBridge.buildMetricsSnapshot(); + deepSeekClient.addMetrics(pluginMetrics); + adaptiveThreshold.addMetric(pluginMetrics); + anomalyDetector.addMetric(pluginMetrics); + timeSeriesPredictor.addMetric(pluginMetrics); + } + MetricsData data = resourceMonitor.collectMetrics(); deepSeekClient.addMetrics(data); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e74b5f0..b707a1d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,8 @@ serverguard: + # 运行模式:plugin(Paper 插件嵌入式)或 external(传统外部监控) + # Paper 插件模式由系统属性 serverguard.mode=plugin 自动设置 + mode: ${SERVERGUARD_MODE:external} + minecraft: service-name: minecraft process-name: paper-1.21.11.jar diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..20ab496 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,47 @@ +name: AluerServerGuard +version: 4.0.0 +main: com.aluer.plugin.AluerPlugin +api-version: '1.21' +description: AI-powered Minecraft PaperMC Server Protection System — 29 anti-cheat modules, DDoS defense, WAF, kernel-based threat fusion +author: Aluer +website: https://github.com/aluer-ai/serverguard +load: STARTUP +prefix: Aluer + +commands: + aluer: + description: Aluer ServerGuard 主控命令 + usage: /aluer + permission: aluer.admin + aluerstatus: + description: 查看 Aluer 实时防护状态 + usage: /aluerstatus + permission: aluer.status + aluerplayers: + description: 查看玩家风险评分列表 + usage: /aluerplayers + permission: aluer.status + aluerblock: + description: 手动封禁玩家/IP + usage: /aluerblock [reason] + permission: aluer.admin + aluerunblock: + description: 手动解封玩家/IP + usage: /aluerunblock + permission: aluer.admin + aluerscan: + description: 手动触发安全扫描 + usage: /aluerscan + permission: aluer.admin + aluerwhitelist: + description: 启用/禁用紧急白名单模式 + usage: /aluerwhitelist + permission: aluer.admin + +permissions: + aluer.admin: + description: Aluer ServerGuard 管理员权限 + default: op + aluer.status: + description: 查看 Aluer 状态 + default: op