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); }
}