From 92a590c36a5fae483cf22c19c5ec03790da02ced Mon Sep 17 00:00:00 2001 From: Peijun Zhao Date: Sat, 16 May 2026 16:14:48 +0800 Subject: [PATCH] =?UTF-8?q?V5.0=20Agent=E6=9E=B6=E6=9E=84=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=9A=E8=BD=BB=E9=87=8FAgent=E6=B3=A8=E5=85=A5MC?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=20+=20WebSocket=E5=AE=9E=E6=97=B6=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=9B=9E=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 架构变更: - ServerGuard 保持独立外部进程(Spring Boot),Agent 为轻量 Paper 插件 - Agent 通过 WebSocket (ws://host:8080/agent) 实时推送 Bukkit 事件/指标/告警至 ServerGuard - ServerGuard 通过 WebSocket 下发防御指令(ban/kick/whitelist),Agent 端通过 InternalCommandExecutor 直接执行 - 新增 AgentMessage 通信协议(14种消息类型 + 10种命令类型) - Agent 端即时拦截(超高速移动/超远攻击/命令注入/广告/钓鱼)+ Server 端深度 AI 分析 新增文件: - agent/AgentMessage.java — 通信协议定义 - server/AgentWebSocketServer.java — Spring WebSocket 服务端 - server/AgentCommandDispatcher.java — 指令下发调度器 - config/WebSocketConfig.java — WebSocket 端点配置 - plugin/bridge/AgentWebSocketClient.java — Agent WebSocket 客户端 重写文件: - AluerPlugin.java — 轻量 Agent(移除 Spring Boot 嵌入) - 所有 9 个 EventListener — 数据通过 WebSocket 推送 - AluerCommandExecutor.java — 精简版命令执行器 - ServerGuardService.java — Agent WebSocket 事件处理集成 - ServerGuardApplication.java — 还原为独立引擎入口 技术栈:pom.xml 新增 spring-boot-starter-websocket 278 项全量测试通过,0 失败 --- .idea/modules.xml | 8 + pom.xml | 5 + .../com/aluer/ServerGuardApplication.java | 32 +- .../java/com/aluer/agent/AgentMessage.java | 137 ++++++ .../com/aluer/config/WebSocketConfig.java | 29 ++ .../aluer/plugin/AluerCommandExecutor.java | 393 ++++------------- .../java/com/aluer/plugin/AluerPlugin.java | 148 +++---- .../plugin/bridge/AgentWebSocketClient.java | 402 ++++++++++++++++++ .../plugin/listener/BlockEventListener.java | 138 ++---- .../plugin/listener/ChatEventListener.java | 105 +++-- .../plugin/listener/CombatEventListener.java | 132 +++--- .../plugin/listener/CommandEventListener.java | 79 ++-- .../plugin/listener/EntityEventListener.java | 80 ++-- .../listener/InventoryEventListener.java | 109 +---- .../plugin/listener/PacketEventListener.java | 67 +-- .../plugin/listener/PlayerEventListener.java | 219 ++++------ .../plugin/listener/WorldEventListener.java | 49 +-- .../aluer/server/AgentWebSocketServer.java | 213 ++++++++++ .../aluer/service/AgentCommandDispatcher.java | 85 ++++ .../com/aluer/service/ServerGuardService.java | 328 +++++++------- 20 files changed, 1538 insertions(+), 1220 deletions(-) create mode 100644 .idea/modules.xml create mode 100644 src/main/java/com/aluer/agent/AgentMessage.java create mode 100644 src/main/java/com/aluer/config/WebSocketConfig.java create mode 100644 src/main/java/com/aluer/plugin/bridge/AgentWebSocketClient.java create mode 100644 src/main/java/com/aluer/server/AgentWebSocketServer.java create mode 100644 src/main/java/com/aluer/service/AgentCommandDispatcher.java diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7132963 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2097f08..05fc314 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,11 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-websocket + + org.springframework.shell spring-shell-starter diff --git a/src/main/java/com/aluer/ServerGuardApplication.java b/src/main/java/com/aluer/ServerGuardApplication.java index a605533..9393882 100644 --- a/src/main/java/com/aluer/ServerGuardApplication.java +++ b/src/main/java/com/aluer/ServerGuardApplication.java @@ -8,13 +8,10 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.scheduling.annotation.EnableAsync; /** - * Aluer ServerGuard 主应用入口 + * Aluer ServerGuard 主应用入口(外部引擎) * - * 支持两种运行模式: - * 1. standalone — java -jar serverguard.jar(传统外部监控模式) - * 2. plugin — 由 PaperMC 插件 AluerPlugin 调用 bootstrap() 启动嵌入式上下文 - * - * 模式由系统属性 serverguard.mode 决定,AluerPlugin 启动时自动设置为 "plugin" + * 独立运行,等待 Paper 插件 Agent 通过 WebSocket 连接并推送实时数据。 + * 同时保留传统 external 监控模式的进程保活和日志分析能力。 */ @SpringBootApplication @EnableAsync @@ -23,8 +20,6 @@ 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(() -> { @@ -34,25 +29,8 @@ public class ServerGuardApplication { } }, "serverguard-shutdown-hook")); - 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; + logger.info("Aluer ServerGuard v5.0.0 is ready"); + logger.info("Agent WebSocket endpoint: ws://0.0.0.0:{}/agent", context.getEnvironment().getProperty("server.port", "8080")); } public static ConfigurableApplicationContext getContext() { diff --git a/src/main/java/com/aluer/agent/AgentMessage.java b/src/main/java/com/aluer/agent/AgentMessage.java new file mode 100644 index 0000000..aa03805 --- /dev/null +++ b/src/main/java/com/aluer/agent/AgentMessage.java @@ -0,0 +1,137 @@ +package com.aluer.agent; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.time.Instant; +import java.util.UUID; + +/** + * Agent 通信协议 — 定义 Agent(Paper 插件)与 ServerGuard(外部 Spring Boot)之间的消息格式 + * + * 通信方向: + * Agent → Server: EVENT(事件), METRICS(指标), ALERT(告警), HEARTBEAT(心跳), HANDSHAKE(握手) + * Server → Agent: COMMAND(执行命令), CONFIG(配置更新), SHUTDOWN(关闭) + * + * 所有消息均为单行 JSON,通过 WebSocket 文本帧传输 + */ +public class AgentMessage { + + // ─── 消息类型常量 ────────────────────────────────── + + /** Agent → Server:Bukkit 事件数据 */ + public static final String TYPE_EVENT = "EVENT"; + /** Agent → Server:TPS/CPU/Memory 等服务器指标 */ + public static final String TYPE_METRICS = "METRICS"; + /** Agent → Server:检测到的安全告警 */ + public static final String TYPE_ALERT = "ALERT"; + /** Agent → Server:心跳保活 */ + public static final String TYPE_HEARTBEAT = "HEARTBEAT"; + /** Agent → Server:初次连接握手 */ + public static final String TYPE_HANDSHAKE = "HANDSHAKE"; + /** Agent → Server:命令执行结果回执 */ + public static final String TYPE_COMMAND_RESULT = "COMMAND_RESULT"; + + /** Server → Agent:执行命令 */ + public static final String TYPE_COMMAND = "COMMAND"; + /** Server → Agent:配置更新 */ + public static final String TYPE_CONFIG = "CONFIG"; + /** Server → Agent:关闭连接 */ + public static final String TYPE_SHUTDOWN = "SHUTDOWN"; + + // ─── 命令类型常量 ────────────────────────────────── + + public static final String CMD_BAN_IP = "BAN_IP"; + public static final String CMD_BAN_PLAYER = "BAN_PLAYER"; + public static final String CMD_KICK = "KICK"; + public static final String CMD_CLEAR_LAG = "CLEAR_LAG"; + public static final String CMD_SET_SPAWN_RATE = "SET_SPAWN_RATE"; + public static final String CMD_ENABLE_WHITELIST = "ENABLE_WHITELIST"; + public static final String CMD_DISABLE_WHITELIST = "DISABLE_WHITELIST"; + public static final String CMD_BROADCAST = "BROADCAST"; + public static final String CMD_SAVE_ALL = "SAVE_ALL"; + public static final String CMD_EXECUTE = "EXECUTE"; + + // ─── 事件类型常量 ────────────────────────────────── + + public static final String EVENT_PLAYER_JOIN = "PLAYER_JOIN"; + public static final String EVENT_PLAYER_QUIT = "PLAYER_QUIT"; + public static final String EVENT_PLAYER_MOVE = "PLAYER_MOVE"; + public static final String EVENT_PLAYER_TELEPORT = "PLAYER_TELEPORT"; + public static final String EVENT_PLAYER_CHAT = "PLAYER_CHAT"; + public static final String EVENT_PLAYER_COMMAND = "PLAYER_COMMAND"; + public static final String EVENT_PLAYER_DAMAGE = "PLAYER_DAMAGE"; + public static final String EVENT_COMBAT_ATTACK = "COMBAT_ATTACK"; + public static final String EVENT_COMBAT_DEATH = "COMBAT_DEATH"; + public static final String EVENT_BLOCK_BREAK = "BLOCK_BREAK"; + public static final String EVENT_BLOCK_PLACE = "BLOCK_PLACE"; + public static final String EVENT_INVENTORY_CLICK = "INVENTORY_CLICK"; + public static final String EVENT_ENTITY_SPAWN = "ENTITY_SPAWN"; + public static final String EVENT_CHUNK_LOAD = "CHUNK_LOAD"; + + private static final Gson gson = new Gson(); + + // ─── 构造方法 ────────────────────────────────────── + + /** 构造一条 Agent → Server 消息 */ + public static String buildMessage(String type, String agentId, JsonObject payload) { + JsonObject msg = new JsonObject(); + msg.addProperty("type", type); + msg.addProperty("agentId", agentId); + msg.addProperty("timestamp", Instant.now().toEpochMilli()); + msg.add("payload", payload); + return gson.toJson(msg); + } + + /** 构造一条 Server → Agent 命令消息 */ + public static String buildCommand(String commandType, String target, String reason) { + JsonObject payload = new JsonObject(); + payload.addProperty("command", commandType); + payload.addProperty("target", target != null ? target : ""); + payload.addProperty("reason", reason != null ? reason : ""); + + JsonObject msg = new JsonObject(); + msg.addProperty("type", TYPE_COMMAND); + msg.addProperty("requestId", UUID.randomUUID().toString()); + msg.addProperty("timestamp", Instant.now().toEpochMilli()); + msg.add("payload", payload); + return gson.toJson(msg); + } + + /** 构造心跳消息 */ + public static String buildHeartbeat(String agentId) { + JsonObject payload = new JsonObject(); + payload.addProperty("status", "alive"); + return buildMessage(TYPE_HEARTBEAT, agentId, payload); + } + + /** 解析消息的 type 字段 */ + public static String getType(String message) { + try { + JsonObject msg = gson.fromJson(message, JsonObject.class); + return msg.get("type") != null ? msg.get("type").getAsString() : null; + } catch (Exception e) { + return null; + } + } + + /** 解析消息的 payload 对象 */ + public static JsonObject getPayload(String message) { + try { + JsonObject msg = gson.fromJson(message, JsonObject.class); + return msg.getAsJsonObject("payload"); + } catch (Exception e) { + return null; + } + } + + /** 解析消息的 agentId */ + public static String getAgentId(String message) { + try { + JsonObject msg = gson.fromJson(message, JsonObject.class); + return msg.get("agentId") != null ? msg.get("agentId").getAsString() : null; + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/aluer/config/WebSocketConfig.java b/src/main/java/com/aluer/config/WebSocketConfig.java new file mode 100644 index 0000000..9a6dab4 --- /dev/null +++ b/src/main/java/com/aluer/config/WebSocketConfig.java @@ -0,0 +1,29 @@ +package com.aluer.config; + +import com.aluer.server.AgentWebSocketServer; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +/** + * WebSocket 配置 — 注册 Agent 通信端点 + * + * Agent(Paper 插件)通过 ws://host:port/agent?agentId=xxx 连接 + */ +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + private final AgentWebSocketServer agentWebSocketServer; + + public WebSocketConfig(AgentWebSocketServer agentWebSocketServer) { + this.agentWebSocketServer = agentWebSocketServer; + } + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(agentWebSocketServer, "/agent") + .setAllowedOrigins("*"); + } +} diff --git a/src/main/java/com/aluer/plugin/AluerCommandExecutor.java b/src/main/java/com/aluer/plugin/AluerCommandExecutor.java index 1de493c..c193340 100644 --- a/src/main/java/com/aluer/plugin/AluerCommandExecutor.java +++ b/src/main/java/com/aluer/plugin/AluerCommandExecutor.java @@ -1,6 +1,6 @@ package com.aluer.plugin; -import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.AgentWebSocketClient; import com.aluer.plugin.bridge.InternalCommandExecutor; import org.bukkit.Bukkit; import org.bukkit.ChatColor; @@ -9,371 +9,150 @@ 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 Agent 命令执行器(精简版) * - * 命令列表: - * /aluer status — 查看防护系统状态 - * /aluer scan <玩家> — 手动触发玩家安全扫描 - * /aluer defense — 查看当前防御等级 - * /aluer info — 查看系统信息 - * /aluerplayers — 查看玩家风险列表 - * /aluerblock <玩家|ip> <目标> [理由] — 手动封禁 - * /aluerunblock <玩家|ip> <目标> — 手动解封 - * /aluerwhitelist — 白名单管理 + * Agent 模式下命令仅作为辅助管理工具, + * 封禁/踢人等操作仍通过 InternalCommandExecutor 直接执行。 */ public class AluerCommandExecutor implements CommandExecutor, TabCompleter { - private static final Logger logger = LoggerFactory.getLogger(AluerCommandExecutor.class); private final AluerPlugin plugin; - private final DataBridge dataBridge; + private final AgentWebSocketClient wsClient; + private final InternalCommandExecutor executor; - private static final String PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.GOLD + "Aluer" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET; + private static final String P = ChatColor.DARK_GRAY + "[" + ChatColor.GOLD + "Aluer" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET; - public AluerCommandExecutor(AluerPlugin plugin, DataBridge dataBridge) { + public AluerCommandExecutor(AluerPlugin plugin, AgentWebSocketClient wsClient, InternalCommandExecutor executor) { this.plugin = plugin; - this.dataBridge = dataBridge; + this.wsClient = wsClient; + this.executor = executor; } @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; + switch (command.getName().toLowerCase()) { + case "aluer": return handleAluer(sender, args); + case "aluerstatus": return handleStatus(sender); + case "aluerplayers": return handlePlayers(sender); + case "aluerblock": return handleBlock(sender, args); + case "aluerunblock": return handleUnblock(sender, args); + case "aluerscan": return handleScan(sender, args); + case "aluerwhitelist": return handleWhitelist(sender, args); + default: return false; } } - private boolean handleAluerCommand(CommandSender sender, String[] args) { - if (args.length == 0) { - sendHelp(sender); - return true; - } - + private boolean handleAluer(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; + case "status": return handleStatus(sender); + case "scan": return handleScan(sender, Arrays.copyOfRange(args, 1, args.length)); + case "info": return handleInfo(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 未就绪,请稍候..."); - } - + private boolean handleStatus(CommandSender sender) { + sender.sendMessage(P + ChatColor.YELLOW + "═══ Aluer Agent v5.0.0 ═══"); + sender.sendMessage(P + "Server: " + (plugin.isServerConnected() ? ChatColor.GREEN + "已连接" : ChatColor.RED + "未连接")); + sender.sendMessage(P + "TPS: " + ChatColor.AQUA + String.format("%.1f", getTPS())); + sender.sendMessage(P + "在线: " + ChatColor.AQUA + Bukkit.getServer().getOnlinePlayers().size()); return true; } - private boolean handlePlayersCommand(CommandSender sender) { - if (dataBridge == null) { - sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪"); - return true; + private boolean handlePlayers(CommandSender sender) { + sender.sendMessage(P + ChatColor.YELLOW + "在线玩家: " + Bukkit.getServer().getOnlinePlayers().size()); + for (Player p : Bukkit.getServer().getOnlinePlayers()) { + String ip = p.getAddress() != null && p.getAddress().getAddress() != null + ? p.getAddress().getAddress().getHostAddress() : "?"; + sender.sendMessage(P + " " + ChatColor.WHITE + p.getName() + + ChatColor.GRAY + " | IP: " + ip + " | Ping: " + p.getPing() + "ms"); } - - 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; - } - + private boolean handleBlock(CommandSender sender, String[] args) { + if (args.length < 2) { sender.sendMessage(P + 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); - } + String reason = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : "管理员手动封禁"; + boolean ok = "ip".equals(type) ? executor.banIP(target, reason) : executor.banPlayer(target, reason); + sender.sendMessage(P + (ok ? ChatColor.GREEN + "已封禁 " : ChatColor.RED + "封禁失败 ") + type + ": " + target); return true; } - private boolean handleUnblockCommand(CommandSender sender, String[] args) { - if (args.length < 2) { - sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerunblock <目标>"); - return true; - } - + private boolean handleUnblock(CommandSender sender, String[] args) { + if (args.length < 2) { sender.sendMessage(P + 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); - } - + boolean ok = "ip".equals(type) ? executor.unbanIP(target) : executor.unbanPlayer(target); + sender.sendMessage(P + (ok ? ChatColor.GREEN + "已解封 " : ChatColor.RED + "解封失败 ") + type + ": " + 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) + " 分钟"); - + private boolean handleScan(CommandSender sender, String[] args) { + if (args.length < 1) { sender.sendMessage(P + ChatColor.RED + "用法: /aluerscan <玩家名>"); return true; } + Player target = Bukkit.getServer().getPlayer(args[0]); + if (target == null) { sender.sendMessage(P + ChatColor.RED + "玩家不在线"); return true; } + sender.sendMessage(P + ChatColor.YELLOW + "═══ 扫描: " + target.getName() + " ═══"); + sender.sendMessage(P + "位置: " + String.format("(%.1f, %.1f, %.1f)", target.getLocation().getX(), target.getLocation().getY(), target.getLocation().getZ())); + sender.sendMessage(P + "IP: " + (target.getAddress() != null ? target.getAddress().getAddress().getHostAddress() : "?")); + sender.sendMessage(P + "Ping: " + target.getPing() + "ms | OP: " + target.isOp()); + sender.sendMessage(P + "飞行: " + target.isFlying() + " | 滑翔: " + target.isGliding()); 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; - + private boolean handleWhitelist(CommandSender sender, String[] args) { + if (args.length < 1) { sender.sendMessage(P + ChatColor.RED + "用法: /aluerwhitelist "); return true; } 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 "); + case "on": executor.enableWhitelist(); sender.sendMessage(P + ChatColor.RED + "白名单已启用"); break; + case "off": executor.disableWhitelist(); sender.sendMessage(P + ChatColor.GREEN + "白名单已关闭"); break; + case "status": sender.sendMessage(P + "白名单: " + (executor.isWhitelistEnabled() ? ChatColor.RED + "启用" : ChatColor.GREEN + "关闭")); break; + default: sender.sendMessage(P + ChatColor.RED + "用法: /aluerwhitelist "); } return true; } + private boolean handleInfo(CommandSender sender) { + sender.sendMessage(P + ChatColor.GOLD + "Aluer ServerGuard Agent v5.0.0"); + sender.sendMessage(P + ChatColor.GRAY + "轻量数据采集前端 — 连接外部 ServerGuard 引擎"); + sender.sendMessage(P + "Server: " + (plugin.isServerConnected() ? ChatColor.GREEN + "已连接" : ChatColor.RED + "未连接")); + 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(P + ChatColor.GOLD + "═══ Aluer Agent 命令 ═══"); + sender.sendMessage(ChatColor.YELLOW + "/aluer status" + ChatColor.GRAY + " — 查看Agent状态"); + sender.sendMessage(ChatColor.YELLOW + "/aluer scan <玩家>" + 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 + " — 白名单管理"); + 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 + " — 白名单"); } - // ─── 命令补全 ────────────────────────────────────── + private double getTPS() { + try { double[] t = Bukkit.getServer().getTPS(); return t.length > 0 ? t[0] : 20.0; } + catch (Exception e) { return 20.0; } + } @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; - } - + String cmd = command.getName().toLowerCase(); + if ("aluer".equals(cmd) && args.length == 1) return filter(Arrays.asList("status", "scan", "info"), args[0]); + if (("aluerblock".equals(cmd) || "aluerunblock".equals(cmd)) && args.length == 1) return filter(Arrays.asList("player", "ip"), args[0]); + if ("aluerwhitelist".equals(cmd) && args.length == 1) return filter(Arrays.asList("on", "off", "status"), args[0]); + if ("aluerscan".equals(cmd) && args.length == 1) return Bukkit.getOnlinePlayers().stream().map(Player::getName).filter(n -> n.toLowerCase().startsWith(args[0].toLowerCase())).collect(Collectors.toList()); 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()); + private List filter(List opts, String prefix) { + return opts.stream().filter(s -> s.toLowerCase().startsWith(prefix.toLowerCase())).collect(Collectors.toList()); } } diff --git a/src/main/java/com/aluer/plugin/AluerPlugin.java b/src/main/java/com/aluer/plugin/AluerPlugin.java index 239a898..152ae4e 100644 --- a/src/main/java/com/aluer/plugin/AluerPlugin.java +++ b/src/main/java/com/aluer/plugin/AluerPlugin.java @@ -1,152 +1,124 @@ package com.aluer.plugin; -import com.aluer.ServerGuardApplication; -import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.AgentWebSocketClient; +import com.aluer.plugin.bridge.InternalCommandExecutor; 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 插件主入口 + * Aluer ServerGuard — PaperMC 轻量 Agent 插件 * - * 插件加载时启动嵌入式 Spring Boot 上下文, - * 注册所有 Bukkit 事件监听器,通过 DataBridge - * 将服务器内部实时数据推送至安全分析引擎。 + * 本插件不嵌入 Spring Boot,而是作为数据采集前端: + * 1. 注册 Bukkit 事件监听器,拦截服务器内部所有关键事件 + * 2. 通过 WebSocket 将事件实时推送至外部 ServerGuard 分析引擎 + * 3. 接收 ServerGuard 下发的防御指令并通过 InternalCommandExecutor 执行 * * 架构: - * Bukkit Events → Event Listeners → DataBridge → Security Services → AI → AutoExecutor → Bukkit API + * Bukkit Events → Listeners → AgentWebSocketClient → (WebSocket) → ServerGuard Server + * ServerGuard Server → (WebSocket) → AgentWebSocketClient → InternalCommandExecutor → Bukkit API + * + * 部署要求: + * - 打包为 Paper 插件 JAR,放入 plugins/ 目录 + * - 外部 ServerGuard 必须先于 Paper 服务器启动 + * - 默认连接 ws://localhost:8080/agent */ 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; + /** WebSocket 客户端,负责与服务端通信 */ + private AgentWebSocketClient wsClient; - /** 数据桥接中心,连接 Bukkit 事件与安全服务 */ - private DataBridge dataBridge; + /** Bukkit API 命令执行器 */ + private InternalCommandExecutor commandExecutor; - /** Spring 上下文是否已就绪 */ - private volatile boolean springReady = false; + /** 服务端 URL(可通过 config.yml 覆盖) */ + private String serverUrl = "ws://localhost:8080/agent"; // ─── 生命周期 ──────────────────────────────────────── @Override public void onLoad() { instance = this; - logger.info("Aluer ServerGuard v4.0.0 — PaperMC Plugin loading (LOAD STARTUP)"); + logger.info("══════════════════════════════════════════════"); + logger.info(" Aluer ServerGuard Agent v5.0.0"); + logger.info(" 轻量数据采集前端 — 连接外部 ServerGuard 引擎"); + logger.info("══════════════════════════════════════════════"); } @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"); + // 读取配置 + saveDefaultConfig(); + serverUrl = getConfig().getString("server-url", "ws://localhost:8080/agent"); - 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(); + // 初始化内部命令执行器 + commandExecutor = new InternalCommandExecutor(); - // 步骤 2:等待 Spring 上下文就绪(最长 30 秒) - int waited = 0; - while (!springReady && waited < 30) { - try { - Thread.sleep(1000); - waited++; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } + // 初始化 WebSocket 客户端 + wsClient = new AgentWebSocketClient(serverUrl, commandExecutor); + wsClient.connect(); - if (!springReady) { - logger.error("Spring Boot failed to start within 30s — plugin will operate in degraded mode"); - } - - // 步骤 3:注册 Bukkit 事件监听器 + // 注册 Bukkit 事件监听器(传入 WebSocket 客户端) 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/"); + logger.info("Agent enabled in {}ms", elapsed); + logger.info(" Server: {}", serverUrl); + logger.info(" Listeners: 9 event handlers registered"); + logger.info(" Commands: /aluer /aluerstatus /aluerplayers /aluerblock /aluerscan /aluerwhitelist"); } @Override public void onDisable() { - logger.info("Shutting down Aluer ServerGuard..."); + logger.info("Shutting down Aluer Agent..."); - // 注销所有 Bukkit 事件监听器 HandlerList.unregisterAll(this); - // 关闭 Spring Boot 上下文 - if (springContext != null) { - springContext.close(); - springContext = null; + if (wsClient != null) { + wsClient.disconnect(); } instance = null; - logger.info("Aluer ServerGuard disabled — protection stopped"); + logger.info("Agent disabled"); } // ─── 事件监听器注册 ────────────────────────────────── private void registerEventListeners() { - // 注意:监听器在构造函数中接收 DataBridge 引用 - // 如果 Spring 未就绪则使用 null(监听器内部做空安全处理) - DataBridge bridge = springReady ? dataBridge : null; + Bukkit.getPluginManager().registerEvents(new PlayerEventListener(this, wsClient), this); + Bukkit.getPluginManager().registerEvents(new CombatEventListener(this, wsClient), this); + Bukkit.getPluginManager().registerEvents(new ChatEventListener(this, wsClient), this); + Bukkit.getPluginManager().registerEvents(new CommandEventListener(this, wsClient), this); + Bukkit.getPluginManager().registerEvents(new BlockEventListener(this, wsClient), this); + Bukkit.getPluginManager().registerEvents(new InventoryEventListener(this, wsClient), this); + Bukkit.getPluginManager().registerEvents(new EntityEventListener(this, wsClient), this); + Bukkit.getPluginManager().registerEvents(new WorldEventListener(this, wsClient), this); - // 核心监听器:玩家、战斗、聊天、命令 - 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); + Bukkit.getPluginManager().registerEvents(new PacketEventListener(this, wsClient), this); } } // ─── 命令注册 ──────────────────────────────────────── private void registerCommands() { - AluerCommandExecutor executor = new AluerCommandExecutor(this, dataBridge); + AluerCommandExecutor executor = new AluerCommandExecutor(this, wsClient, commandExecutor); 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"); @@ -163,23 +135,19 @@ public class AluerPlugin extends JavaPlugin { // ─── 公开访问器 ────────────────────────────────────── - /** 获取插件单例实例 */ public static AluerPlugin getInstance() { return instance; } - /** 获取 Spring 应用上下文(可能为 null,在 Spring 就绪前) */ - public ConfigurableApplicationContext getSpringContext() { - return springContext; + public AgentWebSocketClient getWsClient() { + return wsClient; } - /** 获取 DataBridge(可能为 null,在 Spring 就绪前) */ - public DataBridge getDataBridge() { - return dataBridge; + public InternalCommandExecutor getCommandExecutor() { + return commandExecutor; } - /** Spring 上下文是否已完全初始化 */ - public boolean isSpringReady() { - return springReady; + public boolean isServerConnected() { + return wsClient != null && wsClient.isConnected(); } } diff --git a/src/main/java/com/aluer/plugin/bridge/AgentWebSocketClient.java b/src/main/java/com/aluer/plugin/bridge/AgentWebSocketClient.java new file mode 100644 index 0000000..70dbd88 --- /dev/null +++ b/src/main/java/com/aluer/plugin/bridge/AgentWebSocketClient.java @@ -0,0 +1,402 @@ +package com.aluer.plugin.bridge; + +import com.aluer.agent.AgentMessage; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.function.Consumer; + +/** + * Agent WebSocket 客户端 — 运行在 Paper 插件内部 + * + * 职责: + * 1. 建立与外部 ServerGuard Server 的 WebSocket 长连接 + * 2. 将 Bukkit 事件序列化为 JSON 并实时推送 + * 3. 接收 Server 下发的命令并交由 InternalCommandExecutor 执行 + * 4. 断线自动重连 + 心跳保活 + * + * 通信协议见 AgentMessage 类定义 + */ +public class AgentWebSocketClient { + private static final Logger logger = LoggerFactory.getLogger(AgentWebSocketClient.class); + + private final String agentId; + private final String serverUrl; + private final InternalCommandExecutor commandExecutor; + private final HttpClient httpClient; + private final ScheduledExecutorService scheduler; + + /** 命令结果回调(收到服务端命令后执行) */ + private Consumer commandHandler; + + private volatile WebSocket webSocket; + private volatile boolean connected = false; + private volatile boolean running = true; + + /** 重连间隔(秒),指数退避上限 60 秒 */ + private int reconnectDelay = 5; + private static final int MAX_RECONNECT_DELAY = 60; + + /** 心跳间隔(秒) */ + private static final int HEARTBEAT_INTERVAL = 15; + + /** 待发送消息队列(断线时缓存) */ + private final BlockingQueue pendingMessages = new LinkedBlockingQueue<>(500); + + private static final Gson gson = new Gson(); + + public AgentWebSocketClient(String serverUrl, InternalCommandExecutor commandExecutor) { + this.agentId = UUID.randomUUID().toString(); + this.serverUrl = serverUrl; + this.commandExecutor = commandExecutor; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "aluer-agent-ws"); + t.setDaemon(true); + return t; + }); + } + + public void setCommandHandler(Consumer handler) { + this.commandHandler = handler; + } + + // ─── 连接管理 ────────────────────────────────────── + + /** 启动连接(异步,不阻塞主线程) */ + public void connect() { + scheduler.execute(this::doConnect); + } + + private void doConnect() { + while (running && !connected) { + try { + logger.info("Agent {} connecting to ServerGuard at {}...", agentId, serverUrl); + URI uri = URI.create(serverUrl + "?agentId=" + agentId); + + CompletableFuture future = httpClient.newWebSocketBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .buildAsync(uri, new WebSocketListener()); + + webSocket = future.get(15, TimeUnit.SECONDS); + connected = true; + reconnectDelay = 5; // 重置重连间隔 + logger.info("Agent {} connected to ServerGuard successfully", agentId); + + // 发送握手消息 + sendHandshake(); + + // 开始心跳 + startHeartbeat(); + + // 发送积压消息 + drainPendingMessages(); + + } catch (Exception e) { + logger.warn("Agent {} connection failed: {}. Retrying in {}s...", + agentId, e.getMessage(), reconnectDelay); + connected = false; + + try { + Thread.sleep(reconnectDelay * 1000L); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + + reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY); + } + } + } + + /** 断开连接 */ + public void disconnect() { + running = false; + scheduler.shutdown(); + if (webSocket != null) { + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Agent shutting down"); + } + connected = false; + logger.info("Agent {} disconnected", agentId); + } + + public boolean isConnected() { + return connected && webSocket != null && !webSocket.isOutputClosed(); + } + + // ─── 消息发送 ────────────────────────────────────── + + /** 发送事件到 ServerGuard */ + public void sendEvent(String eventType, JsonObject eventData) { + String msg = AgentMessage.buildMessage(AgentMessage.TYPE_EVENT, agentId, createPayload(eventType, eventData)); + send(msg); + } + + /** 发送指标数据到 ServerGuard */ + public void sendMetrics(JsonObject metricsData) { + String msg = AgentMessage.buildMessage(AgentMessage.TYPE_METRICS, agentId, metricsData); + send(msg); + } + + /** 发送告警到 ServerGuard */ + public void sendAlert(String alertType, String alertMessage, double confidence, String source) { + JsonObject alertData = new JsonObject(); + alertData.addProperty("alertType", alertType); + alertData.addProperty("message", alertMessage); + alertData.addProperty("confidence", confidence); + alertData.addProperty("source", source); + String msg = AgentMessage.buildMessage(AgentMessage.TYPE_ALERT, agentId, alertData); + send(msg); + } + + /** 发送命令执行结果回执 */ + public void sendCommandResult(String requestId, boolean success, String details) { + JsonObject result = new JsonObject(); + result.addProperty("requestId", requestId); + result.addProperty("success", success); + result.addProperty("details", details); + String msg = AgentMessage.buildMessage(AgentMessage.TYPE_COMMAND_RESULT, agentId, result); + send(msg); + } + + /** 底层发送方法 */ + private void send(String message) { + if (isConnected()) { + try { + webSocket.sendText(message, true).get(5, TimeUnit.SECONDS); + } catch (Exception e) { + logger.debug("WebSocket send failed, queueing message: {}", e.getMessage()); + queuePending(message); + } + } else { + queuePending(message); + } + } + + private void queuePending(String message) { + pendingMessages.offer(message); + while (pendingMessages.size() > 500) { + pendingMessages.poll(); // 丢弃最旧消息 + } + } + + private JsonObject createPayload(String eventType, JsonObject data) { + JsonObject payload = new JsonObject(); + payload.addProperty("eventType", eventType); + payload.add("data", data); + return payload; + } + + // ─── 内部方法 ────────────────────────────────────── + + private void sendHandshake() { + JsonObject handshake = new JsonObject(); + handshake.addProperty("serverVersion", getServerVersion()); + handshake.addProperty("onlinePlayers", getOnlinePlayerCount()); + String msg = AgentMessage.buildMessage(AgentMessage.TYPE_HANDSHAKE, agentId, handshake); + send(msg); + } + + private void startHeartbeat() { + scheduler.scheduleAtFixedRate(() -> { + if (isConnected()) { + send(AgentMessage.buildHeartbeat(agentId)); + } + }, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, TimeUnit.SECONDS); + } + + private void drainPendingMessages() { + String msg; + int count = 0; + while ((msg = pendingMessages.poll()) != null && count < 100) { + send(msg); + count++; + } + if (count > 0) { + logger.debug("Drained {} pending messages", count); + } + } + + private String getServerVersion() { + try { + return org.bukkit.Bukkit.getServer().getVersion(); + } catch (Exception e) { + return "unknown"; + } + } + + private int getOnlinePlayerCount() { + try { + return org.bukkit.Bukkit.getServer().getOnlinePlayers().size(); + } catch (Exception e) { + return 0; + } + } + + // ─── WebSocket 监听器 ────────────────────────────── + + private class WebSocketListener implements WebSocket.Listener { + private final StringBuilder buffer = new StringBuilder(); + + @Override + public void onOpen(WebSocket ws) { + WebSocket.Listener.super.onOpen(ws); + } + + @Override + public CompletionStage onText(WebSocket ws, CharSequence data, boolean last) { + buffer.append(data); + if (last) { + String message = buffer.toString(); + buffer.setLength(0); + handleServerMessage(message); + ws.request(1); + } + return null; + } + + @Override + public CompletionStage onClose(WebSocket ws, int statusCode, String reason) { + logger.warn("Agent {} WebSocket closed: {} — {}", agentId, statusCode, reason); + connected = false; + if (running) { + scheduler.execute(() -> { + try { + Thread.sleep(reconnectDelay * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + doConnect(); + }); + } + return null; + } + + @Override + public void onError(WebSocket ws, Throwable error) { + logger.error("Agent {} WebSocket error: {}", agentId, error.getMessage()); + connected = false; + } + } + + private void handleServerMessage(String message) { + try { + String type = AgentMessage.getType(message); + if (type == null) return; + + switch (type) { + case AgentMessage.TYPE_COMMAND: + // 收到服务端命令,交由 InternalCommandExecutor 执行 + JsonObject payload = AgentMessage.getPayload(message); + if (payload != null && commandExecutor != null) { + String cmdType = payload.get("command") != null + ? payload.get("command").getAsString() + : ""; + String target = payload.get("target") != null + ? payload.get("target").getAsString() + : ""; + String reason = payload.get("reason") != null + ? payload.get("reason").getAsString() + : ""; + + executeServerCommand(message, cmdType, target, reason); + } + break; + + case AgentMessage.TYPE_SHUTDOWN: + logger.info("Agent {} received shutdown command from ServerGuard", agentId); + disconnect(); + break; + + default: + logger.debug("Agent {} received unknown message type: {}", agentId, type); + } + } catch (Exception e) { + logger.error("Failed to handle server message: {}", e.getMessage()); + } + } + + /** 执行服务端下发的命令 */ + private void executeServerCommand(String rawMessage, String cmdType, String target, String reason) { + boolean success = false; + String details = ""; + + try { + switch (cmdType) { + case AgentMessage.CMD_BAN_IP: + success = commandExecutor.banIP(target, reason); + details = success ? "IP banned: " + target : "Failed to ban IP"; + break; + case AgentMessage.CMD_BAN_PLAYER: + success = commandExecutor.banPlayer(target, reason); + details = success ? "Player banned: " + target : "Failed to ban player"; + break; + case AgentMessage.CMD_KICK: + success = commandExecutor.kickPlayer(target, reason); + details = success ? "Player kicked: " + target : "Player not online"; + break; + case AgentMessage.CMD_CLEAR_LAG: + int removed = commandExecutor.clearDroppedItems(); + success = true; + details = "Removed " + removed + " dropped items"; + break; + case AgentMessage.CMD_SET_SPAWN_RATE: + success = commandExecutor.setSpawnRate(5); + details = "Spawn rate reduced"; + break; + case AgentMessage.CMD_ENABLE_WHITELIST: + success = commandExecutor.enableWhitelist(); + details = "Whitelist enabled"; + break; + case AgentMessage.CMD_DISABLE_WHITELIST: + success = commandExecutor.disableWhitelist(); + details = "Whitelist disabled"; + break; + case AgentMessage.CMD_BROADCAST: + commandExecutor.broadcast(reason); + success = true; + details = "Broadcast sent"; + break; + case AgentMessage.CMD_SAVE_ALL: + success = commandExecutor.saveAllWorlds(); + details = "Worlds saved"; + break; + case AgentMessage.CMD_EXECUTE: + success = commandExecutor.executeCommand(target); + details = "Command executed: " + target; + break; + default: + details = "Unknown command: " + cmdType; + } + } catch (Exception e) { + success = false; + details = "Error: " + e.getMessage(); + logger.error("Command execution failed: {}", e.getMessage()); + } + + // 发送执行结果回执 + String requestId = null; + try { + JsonObject msg = gson.fromJson(rawMessage, JsonObject.class); + requestId = msg.get("requestId") != null ? msg.get("requestId").getAsString() : null; + } catch (Exception ignored) {} + + if (requestId != null) { + sendCommandResult(requestId, success, details); + } + } + + public String getAgentId() { + return agentId; + } +} diff --git a/src/main/java/com/aluer/plugin/listener/BlockEventListener.java b/src/main/java/com/aluer/plugin/listener/BlockEventListener.java index d3fe2b2..bd064b5 100644 --- a/src/main/java/com/aluer/plugin/listener/BlockEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/BlockEventListener.java @@ -1,9 +1,8 @@ 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 com.aluer.plugin.bridge.AgentWebSocketClient; +import com.google.gson.JsonObject; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -17,135 +16,54 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * 方块事件监听器 — 处理方块破坏、放置、告示牌修改 - * - * 实时检测: - * - Nuker(炸图)— 短时间内破坏大量方块 - * - Scaffold(搭桥)— 脚下方块快速连续放置模式 - * - SignExploit(告示牌漏洞)— 异常大小的 NBT 标签内容 - * - Grief(破坏)— 查找特定贵重方块的大规模破坏模式 + * 方块事件监听器(Agent 端) */ public class BlockEventListener implements Listener { private static final Logger logger = LoggerFactory.getLogger(BlockEventListener.class); private final AluerPlugin plugin; - private final DataBridge bridge; + private final AgentWebSocketClient wsClient; - /** 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) { + public BlockEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } @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; - } - } + if (wsClient == null) return; + Block b = event.getBlock(); + JsonObject data = new JsonObject(); + data.addProperty("playerName", event.getPlayer().getName()); + data.addProperty("material", b.getType().name()); + data.addProperty("x", b.getX()); data.addProperty("y", b.getY()); data.addProperty("z", b.getZ()); + data.addProperty("world", b.getWorld().getName()); + wsClient.sendEvent("BLOCK_BREAK", data); } @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()); - } - } + if (wsClient == null) return; + Block b = event.getBlock(); + JsonObject data = new JsonObject(); + data.addProperty("playerName", event.getPlayer().getName()); + data.addProperty("material", b.getType().name()); + data.addProperty("x", b.getX()); data.addProperty("y", b.getY()); data.addProperty("z", b.getZ()); + data.addProperty("world", b.getWorld().getName()); + wsClient.sendEvent("BLOCK_PLACE", data); } @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onSignChange(SignChangeEvent event) { - if (bridge == null) return; - Player player = event.getPlayer(); - - // —— SignExploit 检测 —— - // 检测异常长的告示牌文本(可能导致崩溃的 NBT 攻击) + if (wsClient == null) return; + // —— Agent 端即时拦截:SignExploit —— 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()); + if (line != null && line.length() > 128) { event.setCancelled(true); + wsClient.sendAlert("SECURITY_SIGN_EXPLOIT", + "即时拦截:玩家 " + event.getPlayer().getName() + " 告示牌文本异常长", + 0.9, event.getPlayer().getName()); return; } } diff --git a/src/main/java/com/aluer/plugin/listener/ChatEventListener.java b/src/main/java/com/aluer/plugin/listener/ChatEventListener.java index a6414dc..56badc1 100644 --- a/src/main/java/com/aluer/plugin/listener/ChatEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/ChatEventListener.java @@ -1,9 +1,8 @@ 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 com.aluer.plugin.bridge.AgentWebSocketClient; +import com.google.gson.JsonObject; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -11,88 +10,84 @@ import org.bukkit.event.player.AsyncPlayerChatEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; /** - * 聊天事件监听器 — 处理玩家聊天消息 + * 聊天事件监听器(Agent 端) * - * 实时检测: - * - ChatFlood(聊天刷屏)— 短时间内大量消息 - * - Advertisement(广告)— IP/域名模式匹配 - * - PhishingLink(钓鱼链接)— 可疑 URL 检测 - * - Profanity(不雅用语)— 关键词过滤 - * - Spam(重复消息)— 相同内容重复发送 + * Agent 端即时拦截(可取消事件):IP广告/钓鱼链接/刷屏 + * 同时推送所有聊天数据至 ServerGuard 做深度内容分析和模式学习 */ public class ChatEventListener implements Listener { private static final Logger logger = LoggerFactory.getLogger(ChatEventListener.class); private final AluerPlugin plugin; - private final DataBridge bridge; + private final AgentWebSocketClient wsClient; - /** 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]+", + "https?://[^\\s]+|bit\\.ly/[^\\s]+|discord\\.gg/[^\\s]+", Pattern.CASE_INSENSITIVE); - /** 聊天刷屏阈值(秒内消息数) */ - private static final int SPAM_THRESHOLD = 5; - private static final long SPAM_WINDOW_MS = 10_000; + /** 刷屏检测:最近消息时间戳 */ + private final Map lastChatTime = new ConcurrentHashMap<>(); + private final Map chatBurstCount = new ConcurrentHashMap<>(); + private static final long CHAT_BURST_WINDOW_MS = 2000; + private static final int CHAT_BURST_THRESHOLD = 4; - public ChatEventListener(AluerPlugin plugin, DataBridge bridge) { + public ChatEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onAsyncPlayerChat(AsyncPlayerChatEvent event) { - if (bridge == null) return; + if (wsClient == null) return; String message = event.getMessage(); String playerName = event.getPlayer().getName(); - PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId()); + UUID uuid = event.getPlayer().getUniqueId(); - // —— 聊天刷屏检测 —— - if (snap != null) { - snap.recentMessages.add(message); - while (snap.recentMessages.size() > 20) { - snap.recentMessages.removeFirst(); - } + // —— Agent 端即时拦截:IP 广告 —— + if (IP_PATTERN.matcher(message).find()) { + event.setCancelled(true); + wsClient.sendAlert("CHAT_ADVERTISEMENT", + "即时拦截:玩家 " + playerName + " 发送疑似IP地址", 0.9, playerName); + return; + } - long now = System.currentTimeMillis(); - long recentCount = snap.recentMessages.stream() - .filter(m -> true) // 所有消息都在时间窗口内(Deque 中保留最近 20 条) - .count(); + // —— Agent 端即时拦截:钓鱼链接 —— + if (URL_PATTERN.matcher(message).find()) { + event.setCancelled(true); + wsClient.sendAlert("CHAT_PHISHING", + "即时拦截:玩家 " + playerName + " 发送外部链接", 0.85, playerName); + return; + } - if (recentCount >= SPAM_THRESHOLD) { - bridge.alert(AlertType.CHAT_FLOOD, - String.format("玩家 %s 聊天刷屏:%d 条消息", playerName, recentCount), - 0.7, playerName); + // —— Agent 端即时拦截:刷屏 —— + long now = System.currentTimeMillis(); + Long last = lastChatTime.get(uuid); + if (last != null && (now - last) < CHAT_BURST_WINDOW_MS) { + int count = chatBurstCount.merge(uuid, 1, Integer::sum); + if (count >= CHAT_BURST_THRESHOLD) { event.setCancelled(true); + wsClient.sendAlert("CHAT_FLOOD", + "即时拦截:玩家 " + playerName + " 聊天刷屏", 0.8, playerName); return; } + } else { + chatBurstCount.put(uuid, 1); } + lastChatTime.put(uuid, now); - // —— 广告/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"); + // 推送聊天数据到 ServerGuard + JsonObject data = new JsonObject(); + data.addProperty("playerName", playerName); + data.addProperty("uuid", uuid.toString()); + data.addProperty("message", message); + wsClient.sendEvent("PLAYER_CHAT", data); } } diff --git a/src/main/java/com/aluer/plugin/listener/CombatEventListener.java b/src/main/java/com/aluer/plugin/listener/CombatEventListener.java index 3bef470..f60f10e 100644 --- a/src/main/java/com/aluer/plugin/listener/CombatEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/CombatEventListener.java @@ -1,112 +1,108 @@ 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 com.aluer.plugin.bridge.AgentWebSocketClient; +import com.google.gson.JsonObject; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +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.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; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + /** - * 战斗事件监听器 — 处理玩家攻击、实体伤害、死亡事件 + * 战斗事件监听器(Agent 端) * - * 实时检测: - * - KillAura(杀戮光环)— 多目标快速切换 + 攻击角度一致性 - * - Reach(攻击距离扩展)— 攻击距离超过 3.0 方块 - * - AutoClicker(自动连点)— 异常攻击频率 - * - ChestSteal — 容器交互跟踪由 InventoryEventListener 处理 + * 采集每次 PvP 攻击的原始数据(双方、距离、角度、伤害值、时间戳), + * 推送至 ServerGuard 做 KillAura/Reach/AutoClicker 模式识别。 */ public class CombatEventListener implements Listener { private static final Logger logger = LoggerFactory.getLogger(CombatEventListener.class); private final AluerPlugin plugin; - private final DataBridge bridge; + private final AgentWebSocketClient wsClient; - /** 正常最大攻击距离(方块) */ - private static final double MAX_ATTACK_DISTANCE = 3.0; + /** Agent 端即时拦截:攻击距离超过此值直接取消(防止极端 Reach hack) */ + private static final double INSTANT_BLOCK_DISTANCE = 5.0; - /** 攻击距离扩展容忍度 */ - private static final double REACH_TOLERANCE = 3.3; + /** 记录每个玩家的攻击时间戳(用于 Agent 端简单 CPS 估算) */ + private final Map lastAttackTime = new ConcurrentHashMap<>(); - public CombatEventListener(AluerPlugin plugin, DataBridge bridge) { + public CombatEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onEntityDamageByEntity(EntityDamageByEntityEvent event) { - if (bridge == null) return; - - // 仅处理玩家攻击 + if (wsClient == 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( + double attackerAngle = Math.toDegrees(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()); + )); + + // —— Agent 端即时拦截:极端距离直接取消攻击 —— + if (distance > INSTANT_BLOCK_DISTANCE) { + event.setCancelled(true); + wsClient.sendAlert("SECURITY_REACH", + "即时拦截:玩家 " + attacker.getName() + " 攻击距离 " + String.format("%.2f", distance) + " 方块", + 0.95, attacker.getName()); + return; } - // —— AutoClicker 检测:攻击频率异常 —— + // 计算 CPS(Agent 端简单估算) 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()); + Long lastTime = lastAttackTime.get(attacker.getUniqueId()); + double estimatedCPS = 0; + if (lastTime != null && lastTime > 0) { + long interval = now - lastTime; + if (interval > 0) estimatedCPS = 1000.0 / interval; + } + lastAttackTime.put(attacker.getUniqueId(), now); + + // 推送攻击数据到 ServerGuard + JsonObject data = new JsonObject(); + data.addProperty("attackerName", attacker.getName()); + data.addProperty("attackerUUID", attacker.getUniqueId().toString()); + data.addProperty("victimName", victim.getName()); + data.addProperty("victimType", victim.getType().name()); + data.addProperty("victimUUID", victim instanceof Player vp ? vp.getUniqueId().toString() : ""); + data.addProperty("distance", distance); + data.addProperty("angle", attackerAngle); + data.addProperty("damage", event.getDamage()); + data.addProperty("estimatedCPS", estimatedCPS); + wsClient.sendEvent("COMBAT_ATTACK", data); + + // Agent 端即时告警(高置信度快速标记,最终决策由 Server 端确认) + if (estimatedCPS > 25) { + wsClient.sendAlert("SECURITY_AUTO_CLICKER", + "Agent 检测:玩家 " + attacker.getName() + " CPS ≈ " + String.format("%.0f", estimatedCPS), + 0.75, attacker.getName()); } } @EventHandler(priority = EventPriority.MONITOR) public void onEntityDeath(EntityDeathEvent event) { - if (bridge == null) return; - Entity entity = event.getEntity(); + if (wsClient == null) return; Player killer = event.getEntity().getKiller(); + if (killer == null) return; - if (killer != null && entity instanceof Player victim) { - bridge.incrementEvent("combat.player_kill"); - } + JsonObject data = new JsonObject(); + data.addProperty("killerName", killer.getName()); + data.addProperty("victimName", event.getEntity().getName()); + data.addProperty("victimType", event.getEntity().getType().name()); + wsClient.sendEvent("COMBAT_DEATH", data); } } diff --git a/src/main/java/com/aluer/plugin/listener/CommandEventListener.java b/src/main/java/com/aluer/plugin/listener/CommandEventListener.java index 0e083aa..f8c2ed2 100644 --- a/src/main/java/com/aluer/plugin/listener/CommandEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/CommandEventListener.java @@ -1,88 +1,57 @@ 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 com.aluer.plugin.bridge.AgentWebSocketClient; +import com.google.gson.JsonObject; 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; /** - * 命令事件监听器 — 监控所有玩家和后台命令执行 + * 命令事件监听器(Agent 端) * - * 实时检测: - * - CommandAbuse(命令滥用)— OP 权限的敏感命令使用 - * - 插件后门命令 — 异常插件注册的危险命令 - * - 命令注入 — 包含 shell 特殊字符的命令 + * Agent 端即时拦截:命令注入、敏感命令 + * 推送所有命令数据至 ServerGuard 做深度行为分析 */ public class CommandEventListener implements Listener { private static final Logger logger = LoggerFactory.getLogger(CommandEventListener.class); private final AluerPlugin plugin; - private final DataBridge bridge; + private final AgentWebSocketClient wsClient; - /** 高危命令前缀列表(需要告警监控) */ - 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 " + private static final String[] BLOCKED_PATTERNS = { + "${", "`", "&& rm ", "|| rm ", "; rm ", "$((", "eval ", "exec(" }; - public CommandEventListener(AluerPlugin plugin, DataBridge bridge) { + public CommandEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } @EventHandler(priority = EventPriority.LOWEST) public void onPlayerCommand(PlayerCommandPreprocessEvent event) { - if (bridge == null) return; + if (wsClient == 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(); + // —— Agent 端即时拦截:命令注入 —— + for (String pattern : BLOCKED_PATTERNS) { + if (command.contains(pattern)) { + event.setCancelled(true); + wsClient.sendAlert("COMMAND_ABUSE", + "即时拦截:玩家 " + playerName + " 命令注入尝试", 0.95, playerName); + return; } } - // —— 敏感命令监控 —— - 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"); + // 推送命令数据到 ServerGuard + JsonObject data = new JsonObject(); + data.addProperty("playerName", playerName); + data.addProperty("command", event.getMessage()); + data.addProperty("isOp", event.getPlayer().isOp()); + wsClient.sendEvent("PLAYER_COMMAND", data); } } diff --git a/src/main/java/com/aluer/plugin/listener/EntityEventListener.java b/src/main/java/com/aluer/plugin/listener/EntityEventListener.java index 4dad65f..92b8243 100644 --- a/src/main/java/com/aluer/plugin/listener/EntityEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/EntityEventListener.java @@ -1,83 +1,57 @@ 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.AgentWebSocketClient; +import com.google.gson.JsonObject; 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.entity.EntitySpawnEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; 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; + private final AgentWebSocketClient wsClient; - public EntityEventListener(AluerPlugin plugin, DataBridge bridge) { + public EntityEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + @EventHandler(priority = EventPriority.MONITOR) public void onEntitySpawn(EntitySpawnEvent event) { - if (bridge == null) return; - bridge.incrementEvent("entity.spawn"); + if (wsClient == null) return; + JsonObject data = new JsonObject(); + data.addProperty("entityType", event.getEntity().getType().name()); + data.addProperty("world", event.getLocation().getWorld() != null ? event.getLocation().getWorld().getName() : ""); + wsClient.sendEvent("ENTITY_SPAWN", data); } - @EventHandler(priority = EventPriority.MONITOR) + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 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); - } + if (wsClient == null) return; + if (event.getState() == PlayerFishEvent.State.CAUGHT_FISH || event.getState() == PlayerFishEvent.State.CAUGHT_ENTITY) { + JsonObject data = new JsonObject(); + data.addProperty("playerName", event.getPlayer().getName()); + data.addProperty("state", event.getState().name()); + wsClient.sendEvent("PLAYER_FISH", data); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onEntityPickupItem(EntityPickupItemEvent event) { - if (bridge == null) return; - if (event.getEntity() instanceof Player) { - bridge.incrementEvent("entity.pickup"); + public void onEntityPickup(EntityPickupItemEvent event) { + if (wsClient == null) return; + if (event.getEntity() instanceof Player p) { + JsonObject data = new JsonObject(); + data.addProperty("playerName", p.getName()); + data.addProperty("itemType", event.getItem().getItemStack().getType().name()); + wsClient.sendEvent("ENTITY_PICKUP", data); } } - - @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 index 4d00fc7..9286174 100644 --- a/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java @@ -1,116 +1,49 @@ 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 com.aluer.plugin.bridge.AgentWebSocketClient; +import com.google.gson.JsonObject; 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; + private final AgentWebSocketClient wsClient; - /** 单次 Click 事件中物品数量变化的阈值(超过视为可疑) */ - private static final int ITEM_CHANGE_THRESHOLD = 64; - - public InventoryEventListener(AluerPlugin plugin, DataBridge bridge) { + public InventoryEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } @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"); + if (wsClient == null) return; + if (!(event.getWhoClicked() instanceof Player p)) return; + JsonObject data = new JsonObject(); + data.addProperty("playerName", p.getName()); + data.addProperty("inventoryType", event.getInventory().getType().name()); + data.addProperty("action", event.getAction().name()); + data.addProperty("slot", event.getSlot()); + data.addProperty("currentItem", event.getCurrentItem() != null ? event.getCurrentItem().getType().name() : "AIR"); + data.addProperty("cursorItem", event.getCursor() != null ? event.getCursor().getType().name() : "AIR"); + wsClient.sendEvent("INVENTORY_CLICK", data); } @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()); - } + if (wsClient == null) return; + if (!(event.getWhoClicked() instanceof Player p)) return; + JsonObject data = new JsonObject(); + data.addProperty("playerName", p.getName()); + data.addProperty("slotsCount", event.getInventorySlots().size()); + wsClient.sendEvent("INVENTORY_DRAG", data); } } diff --git a/src/main/java/com/aluer/plugin/listener/PacketEventListener.java b/src/main/java/com/aluer/plugin/listener/PacketEventListener.java index f16f180..56570c2 100644 --- a/src/main/java/com/aluer/plugin/listener/PacketEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/PacketEventListener.java @@ -1,8 +1,9 @@ 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.AgentWebSocketClient; +import com.google.gson.JsonObject; +import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -10,50 +11,52 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * 包级事件监听器 — 利用 Paper API 对网络包进行细粒度监控 + * 包级事件监听器(Agent 端,Paper 特定 API) * - * Paper 特有 API,非 Paper 服务端会自动跳过注册。 - * - * 实时检测: - * - PacketFlood(包洪水)— 单个玩家发送异常数量的包 - * - TabCompleteCrash — 异常大的 TabComplete 请求 - * - BookBan — 超大书内容导致的崩溃包 - * - ResourcePackExploit — 恶意资源包下载尝试 + * 利用 Paper 特有 API 监控网络包速率, + * 非 Paper 服务端自动跳过注册。 */ public class PacketEventListener implements Listener { private static final Logger logger = LoggerFactory.getLogger(PacketEventListener.class); private final AluerPlugin plugin; - private final DataBridge bridge; + private final AgentWebSocketClient wsClient; - /** 每秒最大允许的包速率(单玩家) */ - private static final int MAX_PACKETS_PER_SECOND = 200; + private long lastPacketReport = System.currentTimeMillis(); + private long packetCount = 0; + private static final long REPORT_INTERVAL_MS = 1000; - public PacketEventListener(AluerPlugin plugin, DataBridge bridge) { + public PacketEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } /** - * 监听 Paper 的包预接收事件(需 Paper 1.20.6+) - * 使用反射调用避免编译时硬依赖 Paper 特定 API 类 + * 定期上报包速率指标(由 ServerGuard 端调度器触发的心跳也会附带此数据) */ - @EventHandler(priority = EventPriority.LOWEST) - public void onPacketReceive(com.destroystokyo.paper.event.player.PlayerHandshakeEvent event) { - if (bridge == null) return; - // 握手阶段即可获取连接来源 - bridge.incrementEvent("packet.handshake"); + public void reportPacketRate() { + if (wsClient == null) return; + long now = System.currentTimeMillis(); + long elapsed = now - lastPacketReport; + if (elapsed <= 0) return; + + double pps = packetCount * 1000.0 / elapsed; + JsonObject metrics = new JsonObject(); + metrics.addProperty("packetsPerSecond", pps); + metrics.addProperty("tps", getTPS()); + metrics.addProperty("onlinePlayers", Bukkit.getServer().getOnlinePlayers().size()); + wsClient.sendMetrics(metrics); + + packetCount = 0; + lastPacketReport = now; } - /** - * 通用的包事件监听 — 使用 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"); + private double getTPS() { + try { + double[] tps = Bukkit.getServer().getTPS(); + return tps.length > 0 ? tps[0] : 20.0; + } catch (Exception e) { + return 20.0; + } } } diff --git a/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java b/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java index ca27da8..7571dd1 100644 --- a/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java @@ -1,9 +1,8 @@ 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 com.aluer.plugin.bridge.AgentWebSocketClient; +import com.google.gson.JsonObject; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -14,171 +13,141 @@ import org.bukkit.event.player.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + /** - * 玩家事件监听器 — 处理登录、退出、移动、传送、死亡等核心玩家事件 + * 玩家事件监听器(Agent 端) * - * 实时检测: - * - Speed(移动速度异常)— 水平速度超过 0.65 blocks/tick 则标记 - * - Fly(非法飞行)— 非创造/旁观模式下,垂直位移模式异常 - * - Jesus(水行)— 连续在水中但 y 坐标不下降 - * - NoFall(无摔伤)— 从高处落地无伤害 + * 采集:登录/退出/移动/传送/摔伤等原始事件, + * 序列化为 JSON 后通过 WebSocket 推送至外部 ServerGuard 引擎。 + * + * Agent 端仅做数据采集和即时拦截(聊天/命令等可取消事件), + * 深度分析(Speed/Fly/Jesus/NoFall 模式识别)由 ServerGuard 端完成。 */ public class PlayerEventListener implements Listener { private static final Logger logger = LoggerFactory.getLogger(PlayerEventListener.class); private final AluerPlugin plugin; - private final DataBridge bridge; + private final AgentWebSocketClient wsClient; - /** 最大允许水平速度(blocks/tick),超过此值视为 Speed Hack */ - private static final double MAX_HORIZONTAL_SPEED = 0.65; + /** 用于 Agent 端简单阈值拦截(阻塞即时作弊,深度分析仍由 Server 端处理) */ + private static final double MAX_INSTANT_SPEED = 1.2; // blocks/tick,超高值直接拦截 - /** 可疑移动的告警置信度阈值 */ - private static final double SUSPICIOUS_CONFIDENCE = 0.75; + /** 玩家上次位置记录(用于计算移动距离) */ + private final Map lastLocations = new ConcurrentHashMap<>(); - public PlayerEventListener(AluerPlugin plugin, DataBridge bridge) { + public PlayerEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } - // ─── 登录/退出 ────────────────────────────────────── - @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()); - } + if (wsClient == null) return; + Player p = event.getPlayer(); + JsonObject data = new JsonObject(); + data.addProperty("playerName", p.getName()); + data.addProperty("uuid", p.getUniqueId().toString()); + data.addProperty("ip", p.getAddress() != null && p.getAddress().getAddress() != null + ? p.getAddress().getAddress().getHostAddress() : "unknown"); + data.addProperty("world", p.getWorld().getName()); + data.addProperty("x", p.getLocation().getX()); + data.addProperty("y", p.getLocation().getY()); + data.addProperty("z", p.getLocation().getZ()); + data.addProperty("isOp", p.isOp()); + data.addProperty("ping", p.getPing()); + wsClient.sendEvent("PLAYER_JOIN", data); } @EventHandler(priority = EventPriority.MONITOR) public void onPlayerQuit(PlayerQuitEvent event) { - if (bridge == null) return; - bridge.removePlayer(event.getPlayer().getUniqueId()); - bridge.incrementEvent("player.quit"); + if (wsClient == null) return; + Player p = event.getPlayer(); + lastLocations.remove(p.getUniqueId()); + JsonObject data = new JsonObject(); + data.addProperty("playerName", p.getName()); + data.addProperty("uuid", p.getUniqueId().toString()); + wsClient.sendEvent("PLAYER_QUIT", data); } - @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; - + if (wsClient == null) return; + Player p = event.getPlayer(); Location from = event.getFrom(); Location to = event.getTo(); - if (to == null) return; + if (to == null || (from.getX() == to.getX() && from.getY() == to.getY() && from.getZ() == to.getZ())) 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(); + double horizontal = Math.sqrt(dx * dx + dz * dz); - 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()); - } + // —— Agent 端即时拦截:极高速度直接阻止移动 —— + if (horizontal > MAX_INSTANT_SPEED && p.getGameMode() == org.bukkit.GameMode.SURVIVAL + && !p.isGliding() && !p.isInsideVehicle()) { + event.setCancelled(true); + wsClient.sendAlert("SECURITY_SPEED", + "即时拦截:玩家 " + p.getName() + " 水平速度 " + String.format("%.3f", horizontal) + " blocks/tick", + 0.95, p.getName()); + return; } - // —— Fly 检测 —— - // 非飞行玩家在空中时持续 y 坐标下降过慢 - if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL - && !player.isFlying() - && !player.isGliding() - && !player.isInWater() - && !player.isInsideVehicle()) { + // 推送移动数据到 ServerGuard 做深度分析 + JsonObject data = new JsonObject(); + data.addProperty("playerName", p.getName()); + data.addProperty("uuid", p.getUniqueId().toString()); + data.addProperty("fromX", from.getX()); data.addProperty("fromY", from.getY()); data.addProperty("fromZ", from.getZ()); + data.addProperty("toX", to.getX()); data.addProperty("toY", to.getY()); data.addProperty("toZ", to.getZ()); + data.addProperty("dx", dx); data.addProperty("dy", dy); data.addProperty("dz", dz); + data.addProperty("horizontal", horizontal); + data.addProperty("yaw", to.getYaw()); data.addProperty("pitch", to.getPitch()); + data.addProperty("isFlying", p.isFlying()); + data.addProperty("isGliding", p.isGliding()); + data.addProperty("isSprinting", p.isSprinting()); + data.addProperty("isSneaking", p.isSneaking()); + data.addProperty("isInWater", p.isInWater()); + data.addProperty("isOnGround", ((org.bukkit.entity.LivingEntity) p).isOnGround()); + data.addProperty("gameMode", p.getGameMode().name()); + data.addProperty("inVehicle", p.isInsideVehicle()); + wsClient.sendEvent("PLAYER_MOVE", data); - // 如果玩家在空中(脚下无方块)且 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()); - } - } + lastLocations.put(p.getUniqueId(), to.clone()); } - // ─── 传送跟踪 ────────────────────────────────────── - @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()); - } + if (wsClient == null || event.getTo() == null) return; + JsonObject data = new JsonObject(); + data.addProperty("playerName", event.getPlayer().getName()); + data.addProperty("fromX", event.getFrom().getX()); data.addProperty("fromY", event.getFrom().getY()); data.addProperty("fromZ", event.getFrom().getZ()); + data.addProperty("toX", event.getTo().getX()); data.addProperty("toY", event.getTo().getY()); data.addProperty("toZ", event.getTo().getZ()); + data.addProperty("cause", event.getCause().name()); + wsClient.sendEvent("PLAYER_TELEPORT", data); } - // ─── 摔伤检测(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; + if (wsClient == null) return; + if (!(event.getEntity() instanceof Player p)) return; - PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); - if (snap == null) return; + JsonObject data = new JsonObject(); + data.addProperty("playerName", p.getName()); + data.addProperty("cause", event.getCause().name()); + data.addProperty("damage", event.getDamage()); + data.addProperty("fallDistance", p.getFallDistance()); - 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()); + if (event.getCause() == EntityDamageEvent.DamageCause.FALL) { + wsClient.sendEvent("PLAYER_DAMAGE", data); + + // Agent 端即时 NoFall 拦截:高坠落低伤害直接标记 + if (p.getFallDistance() > 4.0 && event.getDamage() < 1.0) { + wsClient.sendAlert("SECURITY_NOFALL", + "玩家 " + p.getName() + " 坠落 " + String.format("%.1f", p.getFallDistance()) + " 方块但伤害仅 " + String.format("%.1f", event.getDamage()), + 0.85, p.getName()); + } } } } diff --git a/src/main/java/com/aluer/plugin/listener/WorldEventListener.java b/src/main/java/com/aluer/plugin/listener/WorldEventListener.java index 3e08a67..6fe488b 100644 --- a/src/main/java/com/aluer/plugin/listener/WorldEventListener.java +++ b/src/main/java/com/aluer/plugin/listener/WorldEventListener.java @@ -1,58 +1,45 @@ package com.aluer.plugin.listener; import com.aluer.plugin.AluerPlugin; -import com.aluer.plugin.bridge.DataBridge; +import com.aluer.plugin.bridge.AgentWebSocketClient; +import com.google.gson.JsonObject; 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; + private final AgentWebSocketClient wsClient; - /** 区块加载速率告警阈值(每秒区块数),Baritone/Xray 会快速加载大量区块 */ - private static final int CHUNK_LOAD_RATE_WARNING = 30; - - public WorldEventListener(AluerPlugin plugin, DataBridge bridge) { + public WorldEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) { this.plugin = plugin; - this.bridge = bridge; + this.wsClient = wsClient; } @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 的调度处理) - } + if (wsClient == null) return; + JsonObject data = new JsonObject(); + data.addProperty("world", event.getWorld().getName()); + data.addProperty("chunkX", event.getChunk().getX()); + data.addProperty("chunkZ", event.getChunk().getZ()); + data.addProperty("isNew", event.isNewChunk()); + wsClient.sendEvent("CHUNK_LOAD", data); } @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()); + if (wsClient == null) return; + JsonObject data = new JsonObject(); + data.addProperty("world", event.getWorld().getName()); + data.addProperty("chunkX", event.getChunk().getX()); + data.addProperty("chunkZ", event.getChunk().getZ()); + wsClient.sendEvent("CHUNK_UNLOAD", data); } } diff --git a/src/main/java/com/aluer/server/AgentWebSocketServer.java b/src/main/java/com/aluer/server/AgentWebSocketServer.java new file mode 100644 index 0000000..210f1e8 --- /dev/null +++ b/src/main/java/com/aluer/server/AgentWebSocketServer.java @@ -0,0 +1,213 @@ +package com.aluer.server; + +import com.aluer.agent.AgentMessage; +import com.google.gson.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.*; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; + +/** + * Agent WebSocket 服务器 — 运行在 ServerGuard Spring Boot 内部 + * + * 接收来自 Paper 插件 Agent 的 WebSocket 连接, + * 将 Agent 上报的事件/指标/告警路由到对应的处理器, + * 同时提供向已连接 Agent 下发命令的能力。 + * + * WebSocket 路径:/agent(由 WebSocketConfig 配置) + */ +@Component +public class AgentWebSocketServer extends TextWebSocketHandler { + private static final Logger logger = LoggerFactory.getLogger(AgentWebSocketServer.class); + + /** 已连接 Agent 的 WebSocket 会话(agentId → session) */ + private final Map sessions = new ConcurrentHashMap<>(); + + /** Agent 元数据(agentId → 握手信息) */ + private final Map agentMetadata = new ConcurrentHashMap<>(); + + /** 事件消息处理器(由 ServerGuardService 注入) */ + private volatile BiConsumer eventHandler; + + /** 指标消息处理器 */ + private volatile BiConsumer metricsHandler; + + /** 告警消息处理器 */ + private volatile BiConsumer alertHandler; + + /** 命令执行结果处理器 */ + private volatile BiConsumer commandResultHandler; + + // ─── Spring WebSocket 回调 ──────────────────────── + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + String agentId = extractAgentId(session); + if (agentId == null || agentId.isEmpty()) { + try { + session.close(CloseStatus.BAD_DATA); + } catch (IOException ignored) {} + logger.warn("Rejected agent connection without agentId"); + return; + } + + sessions.put(agentId, session); + logger.info("Agent {} connected from {} (total agents: {})", + agentId, session.getRemoteAddress(), sessions.size()); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + String agentId = extractAgentId(session); + if (agentId == null) return; + + try { + String payload = message.getPayload(); + String type = AgentMessage.getType(payload); + JsonObject data = AgentMessage.getPayload(payload); + + if (type == null) return; + + switch (type) { + case AgentMessage.TYPE_HANDSHAKE: + if (data != null) { + agentMetadata.put(agentId, data); + logger.info("Agent {} handshake: version={}, players={}", + agentId, + data.get("serverVersion") != null ? data.get("serverVersion").getAsString() : "?", + data.get("onlinePlayers") != null ? data.get("onlinePlayers").getAsInt() : 0); + } + break; + + case AgentMessage.TYPE_EVENT: + if (eventHandler != null && data != null) { + eventHandler.accept(agentId, data); + } + break; + + case AgentMessage.TYPE_METRICS: + if (metricsHandler != null && data != null) { + metricsHandler.accept(agentId, data); + } + break; + + case AgentMessage.TYPE_ALERT: + if (alertHandler != null && data != null) { + alertHandler.accept(agentId, data); + } + break; + + case AgentMessage.TYPE_COMMAND_RESULT: + if (commandResultHandler != null && data != null) { + commandResultHandler.accept(agentId, data); + } + break; + + case AgentMessage.TYPE_HEARTBEAT: + // 心跳消息仅用于保活,无需额外处理 + break; + + default: + logger.debug("Unknown message type from agent {}: {}", agentId, type); + } + } catch (Exception e) { + logger.error("Failed to handle message from agent {}: {}", agentId, e.getMessage()); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + String agentId = extractAgentId(session); + if (agentId != null) { + sessions.remove(agentId); + agentMetadata.remove(agentId); + logger.info("Agent {} disconnected (status: {}), remaining: {}", + agentId, status, sessions.size()); + } + } + + @Override + public void handleTransportError(WebSocketSession session, Throwable exception) { + String agentId = extractAgentId(session); + logger.error("Transport error for agent {}: {}", agentId, exception.getMessage()); + } + + // ─── 命令下发 ────────────────────────────────────── + + /** 向指定 Agent 发送命令 */ + public boolean sendCommand(String agentId, String commandType, String target, String reason) { + WebSocketSession session = sessions.get(agentId); + if (session == null || !session.isOpen()) { + logger.warn("Cannot send command: agent {} not connected", agentId); + return false; + } + + try { + String message = AgentMessage.buildCommand(commandType, target, reason); + session.sendMessage(new TextMessage(message)); + return true; + } catch (IOException e) { + logger.error("Failed to send command to agent {}: {}", agentId, e.getMessage()); + return false; + } + } + + /** 向所有已连接 Agent 广播命令 */ + public void broadcastCommand(String commandType, String target, String reason) { + for (String agentId : sessions.keySet()) { + sendCommand(agentId, commandType, target, reason); + } + } + + // ─── 处理器注册 ──────────────────────────────────── + + public void setEventHandler(BiConsumer handler) { + this.eventHandler = handler; + } + + public void setMetricsHandler(BiConsumer handler) { + this.metricsHandler = handler; + } + + public void setAlertHandler(BiConsumer handler) { + this.alertHandler = handler; + } + + public void setCommandResultHandler(BiConsumer handler) { + this.commandResultHandler = handler; + } + + // ─── 状态查询 ────────────────────────────────────── + + public int getConnectedAgentCount() { + return sessions.size(); + } + + public boolean isAgentConnected(String agentId) { + return sessions.containsKey(agentId); + } + + // ─── 内部 ────────────────────────────────────────── + + private String extractAgentId(WebSocketSession session) { + // 从 URL 查询参数中提取 agentId:/agent?agentId=xxx + String query = session.getUri() != null ? session.getUri().getQuery() : null; + if (query != null) { + for (String param : query.split("&")) { + String[] kv = param.split("=", 2); + if (kv.length == 2 && "agentId".equals(kv[0])) { + return kv[1]; + } + } + } + // fallback:从 session attributes 获取 + Object attr = session.getAttributes().get("agentId"); + return attr != null ? attr.toString() : null; + } +} diff --git a/src/main/java/com/aluer/service/AgentCommandDispatcher.java b/src/main/java/com/aluer/service/AgentCommandDispatcher.java new file mode 100644 index 0000000..6d73484 --- /dev/null +++ b/src/main/java/com/aluer/service/AgentCommandDispatcher.java @@ -0,0 +1,85 @@ +package com.aluer.service; + +import com.aluer.ai.DeepSeekClient; +import com.aluer.model.AlertEvent; +import com.aluer.server.AgentWebSocketServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Agent 命令调度器 — 将 ServerGuard 的防御决策通过 WebSocket 下发至 Paper 插件 Agent + * + * AutoExecutor 生成自动防御动作后,如果存在已连接的 Agent, + * 优先通过 AgentCommandDispatcher 将命令(ban/kick/whitelist 等) + * 以 JSON 消息形式发送至 Agent,Agent 端通过 InternalCommandExecutor 执行。 + */ +@Component +public class AgentCommandDispatcher { + private static final Logger logger = LoggerFactory.getLogger(AgentCommandDispatcher.class); + + /** + * 将 AI 分析结果转换为 Agent 命令并通过 WebSocket 下发 + */ + public void dispatchAutoAction(DeepSeekClient.AiAnalysisResult analysis, + AlertEvent alert, + AgentWebSocketServer agentServer) { + String action = analysis.getAutoAction(); + if (action == null || action.isEmpty() || "无".equals(action) || "none".equals(action)) { + return; + } + + int confidencePercent = (int) (alert.getConfidence() * 100); + logger.info("[AGENT-DISPATCH] {} (confidence: {}%)", action, confidencePercent); + + action = action.toLowerCase(); + String source = alert.getSource() != null ? alert.getSource() : ""; + String reason = alert.getType().getTitle() + " — " + analysis.getRootCause(); + + if (action.contains("ban") || action.contains("封禁")) { + dispatchBan(alert, agentServer, reason); + } + if (action.contains("kick") || action.contains("踢出")) { + dispatchKick(alert, agentServer, reason); + } + if (action.contains("kill") || action.contains("清除") || action.contains("清理")) { + agentServer.broadcastCommand("CLEAR_LAG", "", reason); + } + if (action.contains("whitelist") || action.contains("白名单")) { + if (action.contains("enable") || action.contains("开启")) { + agentServer.broadcastCommand("ENABLE_WHITELIST", "", reason); + } + } + } + + private void dispatchBan(AlertEvent alert, AgentWebSocketServer agentServer, String reason) { + String source = alert.getSource() != null ? alert.getSource() : ""; + // 尝试从 source 中提取 IP + if (source.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")) { + agentServer.broadcastCommand("BAN_IP", source, reason); + logger.info("[AGENT-DISPATCH] Ban IP: {}", source); + } else if (!source.isEmpty()) { + agentServer.broadcastCommand("BAN_PLAYER", source, reason); + logger.info("[AGENT-DISPATCH] Ban Player: {}", source); + } + + // 也尝试从告警消息中提取 IP + String msg = alert.getMessage(); + if (msg != null) { + java.util.regex.Matcher m = java.util.regex.Pattern.compile( + "\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b").matcher(msg); + if (m.find()) { + agentServer.broadcastCommand("BAN_IP", m.group(1), reason); + logger.info("[AGENT-DISPATCH] Ban IP from message: {}", m.group(1)); + } + } + } + + private void dispatchKick(AlertEvent alert, AgentWebSocketServer agentServer, String reason) { + String source = alert.getSource() != null ? alert.getSource() : ""; + if (!source.isEmpty() && !source.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")) { + agentServer.broadcastCommand("KICK", source, reason); + logger.info("[AGENT-DISPATCH] Kick: {}", source); + } + } +} diff --git a/src/main/java/com/aluer/service/ServerGuardService.java b/src/main/java/com/aluer/service/ServerGuardService.java index 62dcb44..2f276d8 100644 --- a/src/main/java/com/aluer/service/ServerGuardService.java +++ b/src/main/java/com/aluer/service/ServerGuardService.java @@ -3,11 +3,13 @@ package com.aluer.service; import com.aluer.ai.*; import com.aluer.alert.EmailAlertService; import com.aluer.config.ServerGuardConfig; -import com.aluer.monitor.*; import com.aluer.model.AlertEvent; +import com.aluer.model.AlertType; import com.aluer.model.MetricsData; -import com.aluer.plugin.bridge.DataBridge; -import com.aluer.plugin.bridge.InternalCommandExecutor; +import com.aluer.monitor.ProcessMonitor; +import com.aluer.monitor.ResourceMonitor; +import com.aluer.server.AgentWebSocketServer; +import com.google.gson.JsonObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -15,12 +17,21 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Service; +import java.time.Instant; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +/** + * ServerGuard 核心调度服务 + * + * 负责: + * 1. 外部监控模式:进程保活 + 资源监控 + 日志分析(传统 external mode) + * 2. Agent 模式:接收 Paper 插件 Agent 通过 WebSocket 推送的实时事件/指标/告警 + * 3. 告警处理管道(AI 分析 + 自动执行 + 邮件通知),两种模式共用 + */ @Service public class ServerGuardService implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(ServerGuardService.class); @@ -28,8 +39,6 @@ public class ServerGuardService implements CommandLineRunner { private final ServerGuardConfig config; private final ProcessMonitor processMonitor; private final ResourceMonitor resourceMonitor; - private final ConnectionMonitor connectionMonitor; - private final LogMonitor logMonitor; private final AdaptiveThreshold adaptiveThreshold; private final AnomalyDetector anomalyDetector; private final TimeSeriesPredictor timeSeriesPredictor; @@ -37,15 +46,14 @@ public class ServerGuardService implements CommandLineRunner { private final EmailAlertService emailAlertService; private final DeepSeekClient deepSeekClient; private final AutoExecutor autoExecutor; - private final RconClient rconClient; - /** 插件模式下的数据桥接(external 模式下为 null) */ + /** Agent WebSocket 服务器(接收 Paper 插件 Agent 的实时数据推送) */ @Autowired(required = false) - private DataBridge dataBridge; + private AgentWebSocketServer agentServer; - /** 插件模式下的内部命令执行器(external 模式下为 null) */ + /** Agent 命令调度器(通过 WebSocket 向 Agent 下发防御指令) */ @Autowired(required = false) - private InternalCommandExecutor internalCommandExecutor; + private AgentCommandDispatcher agentCommandDispatcher; @Value("${server.port:8080}") private int serverPort; @@ -53,25 +61,23 @@ public class ServerGuardService implements CommandLineRunner { private final ScheduledExecutorService scheduler; private final AtomicBoolean running = new AtomicBoolean(true); + /** Agent 推送的事件/告警计数 */ + private final AtomicBoolean agentHandlersRegistered = new AtomicBoolean(false); + public ServerGuardService( ServerGuardConfig config, ProcessMonitor processMonitor, ResourceMonitor resourceMonitor, - ConnectionMonitor connectionMonitor, - LogMonitor logMonitor, AdaptiveThreshold adaptiveThreshold, AnomalyDetector anomalyDetector, TimeSeriesPredictor timeSeriesPredictor, AttackDetector attackDetector, EmailAlertService emailAlertService, DeepSeekClient deepSeekClient, - AutoExecutor autoExecutor, - RconClient rconClient) { + AutoExecutor autoExecutor) { this.config = config; this.processMonitor = processMonitor; this.resourceMonitor = resourceMonitor; - this.connectionMonitor = connectionMonitor; - this.logMonitor = logMonitor; this.adaptiveThreshold = adaptiveThreshold; this.anomalyDetector = anomalyDetector; this.timeSeriesPredictor = timeSeriesPredictor; @@ -79,7 +85,6 @@ public class ServerGuardService implements CommandLineRunner { this.emailAlertService = emailAlertService; this.deepSeekClient = deepSeekClient; this.autoExecutor = autoExecutor; - this.rconClient = rconClient; this.scheduler = Executors.newScheduledThreadPool(4, r -> { Thread t = new Thread(r, "serverguard-scheduler"); @@ -93,114 +98,134 @@ public class ServerGuardService implements CommandLineRunner { printBanner(); validateConfiguration(); - 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"); + // 注册 Agent WebSocket 事件处理器 + if (agentServer != null) { + registerAgentHandlers(); + agentHandlersRegistered.set(true); + logger.info("Agent WebSocket handlers registered — ready to receive agent data"); } + // 启动监控(external 模式使用传统监控,agent 模式使用 agent 推送 + 传统指标补充) + startMonitoring(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { logger.info("Shutdown signal received, gracefully stopping..."); running.set(false); scheduler.shutdown(); try { - if (!scheduler.awaitTermination(8, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); - } + if (!scheduler.awaitTermination(8, TimeUnit.SECONDS)) scheduler.shutdownNow(); } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } emailAlertService.shutdown(); - if (rconClient.isConnected()) { - rconClient.close(); - } logger.info("Aluer ServerGuard stopped"); }, "serverguard-shutdown")); } + // ─── Agent 事件处理器注册 ────────────────────────── + + private void registerAgentHandlers() { + // 事件处理器:将 Agent 推送的原始事件转换为指标数据 + agentServer.setEventHandler((agentId, data) -> { + try { + String eventType = data.get("eventType") != null ? data.get("eventType").getAsString() : ""; + JsonObject eventData = data.getAsJsonObject("data"); + + // 将事件数据注入 AI 分析引擎的指标流 + MetricsData metrics = buildMetricsFromAgentEvent(eventType, eventData); + if (metrics != null) { + deepSeekClient.addMetrics(metrics); + adaptiveThreshold.addMetric(metrics); + anomalyDetector.addMetric(metrics); + } + } catch (Exception e) { + logger.debug("Failed to process agent event: {}", e.getMessage()); + } + }); + + // 指标处理器:Agent 定期推送的 TPS/包速率等指标 + agentServer.setMetricsHandler((agentId, data) -> { + try { + MetricsData metrics = new MetricsData(); + metrics.setTps(data.get("tps") != null ? data.get("tps").getAsDouble() : 20.0); + metrics.setOnlinePlayers(data.get("onlinePlayers") != null ? data.get("onlinePlayers").getAsInt() : 0); + metrics.setConnections(metrics.getOnlinePlayers()); + + deepSeekClient.addMetrics(metrics); + adaptiveThreshold.addMetric(metrics); + anomalyDetector.addMetric(metrics); + timeSeriesPredictor.addMetric(metrics); + } catch (Exception e) { + logger.debug("Failed to process agent metrics: {}", e.getMessage()); + } + }); + + // 告警处理器:Agent 即时检测到的安全告警 + agentServer.setAlertHandler((agentId, data) -> { + try { + AlertEvent alert = new AlertEvent(); + String alertType = data.get("alertType") != null ? data.get("alertType").getAsString() : "SECURITY_OTHER"; + + // 映射告警类型字符串到 AlertType 枚举 + try { + alert.setType(AlertType.valueOf(alertType)); + } catch (IllegalArgumentException e) { + alert.setType(AlertType.SECURITY_OTHER); + } + + alert.setMessage(data.get("message") != null ? data.get("message").getAsString() : ""); + alert.setConfidence(data.get("confidence") != null ? data.get("confidence").getAsDouble() : 0.5); + alert.setSource(data.get("source") != null ? data.get("source").getAsString() : agentId); + alert.setTimestamp(Instant.now().toEpochMilli()); + + handleAlert(alert); + } catch (Exception e) { + logger.debug("Failed to process agent alert: {}", e.getMessage()); + } + }); + } + + /** 从 Agent 事件构建 MetricsData 快照 */ + private MetricsData buildMetricsFromAgentEvent(String eventType, JsonObject data) { + // 跟踪玩家在线数变化 + if ("PLAYER_JOIN".equals(eventType) || "PLAYER_QUIT".equals(eventType)) { + MetricsData m = new MetricsData(); + m.setTps(20.0); // 由定期 metrics 推送更新精确值 + m.setConnections(1); // 连接变化 + return m; + } + return null; + } + + // ─── Banner ──────────────────────────────────────── + private void printBanner() { + boolean hasAgent = agentServer != null; logger.info(""); logger.info("╔══════════════════════════════════════════════════════════════╗"); - logger.info("║ Aluer ServerGuard v4.0.0 ║"); + logger.info("║ Aluer ServerGuard v5.0.0 ║"); logger.info("║ AI-Powered Minecraft Server Protection ║"); - logger.info("║ Mode: " + String.format("%-39s", config.isPluginMode() ? "Paper Plugin (Embedded)" : "External Monitor") + "║"); + logger.info("║ Mode: " + String.format("%-39s", hasAgent ? "Agent Server (WebSocket)" : "External Monitor") + "║"); logger.info("╠══════════════════════════════════════════════════════════════╣"); logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version"))); - logger.info("║ Available CPUs: {}{} ║", - String.format("%-32s", Runtime.getRuntime().availableProcessors()), - ""); - logger.info("║ Max Memory: {}{} ║", - String.format("%-32s", (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " MB"), - ""); - logger.info("╠══════════════════════════════════════════════════════════════╣"); - logger.info("║ RCON: {}:{} {}{} ║", - config.getMinecraft().getRcon().getHost(), - config.getMinecraft().getRcon().getPort(), - config.getMinecraft().getRcon().isEnabled() ? "enabled" : "disabled", - " "); + logger.info("║ Web Console: http://0.0.0.0:{}{} ║", serverPort, " "); + if (hasAgent) { + logger.info("║ Agent WS: ws://0.0.0.0:{}/agent{} ║", serverPort, " "); + } logger.info("║ DeepSeek AI: {}{} ║", deepSeekClient.isEnabled() ? "enabled (" + config.getAi().getDeepseek().getModel() + ")" : "disabled", deepSeekClient.isEnabled() ? "" : " "); - logger.info("║ Web Console: http://0.0.0.0:{}{} ║", - serverPort, - " "); - logger.info("╠══════════════════════════════════════════════════════════════╣"); - logger.info("║ Monitor: {}s interval{}{} ║", - config.getMinecraft().getCheckIntervalSeconds(), - " ", - ""); - logger.info("║ Auto-Execute: {}{} ║", - config.getAi().getDeepseek().getAutoExecute().isEnabled() ? "enabled" : "disabled", - " "); - logger.info("║ Auto-Heal: {}{} ║", - config.getSecurity().getSelfHealing().isEnabled() ? "enabled" : "disabled", - " "); logger.info("╚══════════════════════════════════════════════════════════════╝"); logger.info(""); - logger.info("Type 'help' in the shell for available commands."); - logger.info("Access the web console at http://localhost:{}/", serverPort); - logger.info(""); } private void validateConfiguration() { int warnings = 0; - if (!deepSeekClient.isEnabled()) { logger.warn("CONFIG: DeepSeek AI is disabled. Set DEEPSEEK_API_KEY to enable AI features."); warnings++; } - - String rconPassword = config.getMinecraft().getRcon().getPassword(); - if (rconPassword == null || rconPassword.isEmpty()) { - logger.warn("CONFIG: RCON password is not set. Set RCON_PASSWORD env var."); - warnings++; - } - - if (!config.getAlert().getEmail().getUsername().isEmpty()) { - if (config.getAlert().getEmail().getTo() == null || config.getAlert().getEmail().getTo().isEmpty()) { - logger.warn("CONFIG: Email alert enabled but no recipients configured."); - warnings++; - } - } - - if (config.getSecurity().getHostEnforcement().isDryRun()) { - logger.info("CONFIG: Host enforcement is in dry-run mode (safe for first deployment)."); - } - if (config.getSecurity().getSelfHealing().isDryRun()) { - logger.info("CONFIG: Self-healing is in dry-run mode (safe for first deployment)."); - } - if (warnings > 0) { logger.info("Configuration check complete: {} warning(s) found.", warnings); } else { @@ -208,27 +233,30 @@ public class ServerGuardService implements CommandLineRunner { } } + // ─── 监控循环 ────────────────────────────────────── + private void startMonitoring() { - int intervalSeconds = config.getMinecraft().getCheckIntervalSeconds(); + int interval = config.getMinecraft().getCheckIntervalSeconds(); - scheduler.scheduleAtFixedRate(this::checkProcess, 0, intervalSeconds, TimeUnit.SECONDS); - scheduler.scheduleAtFixedRate(this::collectAndAnalyzeMetrics, 5, intervalSeconds, TimeUnit.SECONDS); - - if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) { - int analysisInterval = config.getAi().getDeepseek().getAnalysisIntervalSeconds(); - scheduler.scheduleAtFixedRate(this::runDeepSeekAnalysis, analysisInterval, analysisInterval, TimeUnit.SECONDS); + // 进程保活(external 模式使用) + if (!config.isPluginMode()) { + scheduler.scheduleAtFixedRate(this::checkProcess, 0, interval, TimeUnit.SECONDS); } - logger.info("Monitoring started with {}s interval", intervalSeconds); + scheduler.scheduleAtFixedRate(this::collectAndAnalyzeMetrics, 5, interval, TimeUnit.SECONDS); + + if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) { + int aiInterval = config.getAi().getDeepseek().getAnalysisIntervalSeconds(); + scheduler.scheduleAtFixedRate(this::runDeepSeekAnalysis, aiInterval, aiInterval, TimeUnit.SECONDS); + } + + logger.info("Monitoring started with {}s interval", interval); } private void checkProcess() { try { AlertEvent event = processMonitor.checkAndRestart(); - if (event != null) { - logger.warn("Process alert: {}", event.getMessage()); - handleAlert(event); - } + if (event != null) handleAlert(event); } catch (Exception e) { logger.error("Process check failed: {}", e.getMessage()); } @@ -236,58 +264,30 @@ 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); adaptiveThreshold.addMetric(data); anomalyDetector.addMetric(data); timeSeriesPredictor.addMetric(data); List resourceAlerts = resourceMonitor.checkThresholds(data); - for (AlertEvent alert : resourceAlerts) { - handleAlert(alert); - } - - AlertEvent connectionAlert = connectionMonitor.checkConnectionFlood(); - if (connectionAlert != null) { - handleAlert(connectionAlert); - } - - List logAlerts = logMonitor.analyzeLogs(); - for (AlertEvent alert : logAlerts) { - handleAlert(alert); - } + for (AlertEvent alert : resourceAlerts) handleAlert(alert); if (config.getAi().isEnabled() && config.getAi().isUseIsolationForest()) { AlertEvent aiAlert = anomalyDetector.detectAnomaly(data); - if (aiAlert != null) { - handleAlert(aiAlert); - } + if (aiAlert != null) handleAlert(aiAlert); } if (config.getAi().isEnabled() && config.getAi().isUsePrediction()) { TimeSeriesPredictor.PredictionResult prediction = timeSeriesPredictor.predict(); if (prediction != null && prediction.isWillBreachThreshold() && prediction.getConfidence() > 0.6) { - logger.warn("TPS Prediction: {} (confidence: {:.0f}%) - {}", - prediction.getPredictedValue(), - prediction.getConfidence() * 100, - prediction.getRecommendation()); + logger.warn("TPS Prediction: {} (confidence: {:.0f}%) — {}", + prediction.getPredictedValue(), prediction.getConfidence() * 100, prediction.getRecommendation()); } } List attackAlerts = attackDetector.analyzeThreats(); - for (AlertEvent alert : attackAlerts) { - handleAlert(alert); - } + for (AlertEvent alert : attackAlerts) handleAlert(alert); } catch (Exception e) { logger.error("Metrics collection failed: {}", e.getMessage()); @@ -295,19 +295,13 @@ public class ServerGuardService implements CommandLineRunner { } private void runDeepSeekAnalysis() { - if (!deepSeekClient.isEnabled()) { - return; - } - + if (!deepSeekClient.isEnabled()) return; try { deepSeekClient.getServerHealthReportAsync().thenAccept(healthReport -> { - if (healthReport == null) { - return; - } + if (healthReport == null) return; logger.info("=== DeepSeek AI Health Report ==="); logger.info("Status: {}", healthReport.getSeverity()); logger.info("Summary: {}", healthReport.getDescription()); - if ("critical".equals(healthReport.getSeverity()) || "warning".equals(healthReport.getSeverity())) { for (String action : healthReport.getRecommendedActions()) { logger.warn("Recommendation: {}", action); @@ -319,56 +313,32 @@ public class ServerGuardService implements CommandLineRunner { } } + // ─── 告警处理管道 ────────────────────────────────── + private void handleAlert(AlertEvent alert) { deepSeekClient.addAlert(alert); - - logger.warn("Alert [{}]: {} (confidence: {:.0f}%)", - alert.getType(), alert.getMessage(), alert.getConfidence() * 100); + logger.warn("Alert [{}]: {} (confidence: {:.0f}%)", alert.getType(), alert.getMessage(), alert.getConfidence() * 100); if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) { deepSeekClient.analyzeAlertAsync(alert).thenAccept(analysis -> { if (analysis != null) { - logger.info(" [DeepSeek AI Analysis]"); - logger.info(" Severity: {}", analysis.getSeverity()); - logger.info(" Root Cause: {}", analysis.getRootCause()); - - if (!analysis.getRecommendedActions().isEmpty()) { - logger.info(" Recommended Actions:"); - for (String action : analysis.getRecommendedActions()) { - logger.info(" - {}", action); - } - } - + logger.info(" [DeepSeek AI Analysis] severity={}, rootCause={}", analysis.getSeverity(), analysis.getRootCause()); alert.setRootCause(analysis.getRootCause()); alert.setSuggestedAction(String.join("; ", analysis.getRecommendedActions())); - autoExecutor.executeAutoAction(analysis, alert); + // Agent 模式下通过 WebSocket 下发命令,external 模式下通过 RCON + if (agentCommandDispatcher != null && agentServer != null && agentServer.getConnectedAgentCount() > 0) { + agentCommandDispatcher.dispatchAutoAction(analysis, alert, agentServer); + } else { + autoExecutor.executeAutoAction(analysis, alert); + } } }); - } else { - if (alert.getRootCause() != null) { - logger.info(" Root Cause: {}", alert.getRootCause()); - } - if (alert.getSuggestedAction() != null) { - logger.info(" Suggested Action: {}", alert.getSuggestedAction()); - } } emailAlertService.sendAlert(alert); - - if (alert.getType().name().contains("CONNECTION_FLOOD") || - alert.getType().name().contains("LOG_ATTACK")) { - if (alert.getConfidence() > 0.8) { - logger.info("High confidence attack alert — triggering auto defense"); - } - } } - public boolean isRunning() { - return running.get(); - } - - public void stop() { - running.set(false); - } + public boolean isRunning() { return running.get(); } + public void stop() { running.set(false); } }