V5.0 Agent架构重构:轻量Agent注入MC进程 + WebSocket实时数据回传

架构变更:
- ServerGuard 保持独立外部进程(Spring Boot),Agent 为轻量 Paper 插件
- Agent 通过 WebSocket (ws://host:8080/agent) 实时推送 Bukkit 事件/指标/告警至 ServerGuard
- ServerGuard 通过 WebSocket 下发防御指令(ban/kick/whitelist),Agent 端通过 InternalCommandExecutor 直接执行
- 新增 AgentMessage 通信协议(14种消息类型 + 10种命令类型)
- Agent 端即时拦截(超高速移动/超远攻击/命令注入/广告/钓鱼)+ Server 端深度 AI 分析

新增文件:
- agent/AgentMessage.java — 通信协议定义
- server/AgentWebSocketServer.java — Spring WebSocket 服务端
- server/AgentCommandDispatcher.java — 指令下发调度器
- config/WebSocketConfig.java — WebSocket 端点配置
- plugin/bridge/AgentWebSocketClient.java — Agent WebSocket 客户端

重写文件:
- AluerPlugin.java — 轻量 Agent(移除 Spring Boot 嵌入)
- 所有 9 个 EventListener — 数据通过 WebSocket 推送
- AluerCommandExecutor.java — 精简版命令执行器
- ServerGuardService.java — Agent WebSocket 事件处理集成
- ServerGuardApplication.java — 还原为独立引擎入口

技术栈:pom.xml 新增 spring-boot-starter-websocket
278 项全量测试通过,0 失败
This commit is contained in:
Peijun Zhao
2026-05-16 16:14:48 +08:00
parent 030ced87f9
commit 92a590c36a
20 changed files with 1538 additions and 1220 deletions
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/serverguard.iml" filepath="$PROJECT_DIR$/serverguard.iml" />
</modules>
</component>
</project>
+5
View File
@@ -47,6 +47,11 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.shell</groupId> <groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId> <artifactId>spring-shell-starter</artifactId>
@@ -8,13 +8,10 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
/** /**
* Aluer ServerGuard 主应用入口 * Aluer ServerGuard 主应用入口(外部引擎)
* *
* 支持两种运行模式: * 独立运行,等待 Paper 插件 Agent 通过 WebSocket 连接并推送实时数据。
* 1. standalone — java -jar serverguard.jar(传统外部监控模式) * 同时保留传统 external 监控模式的进程保活和日志分析能力。
* 2. plugin — 由 PaperMC 插件 AluerPlugin 调用 bootstrap() 启动嵌入式上下文
*
* 模式由系统属性 serverguard.mode 决定,AluerPlugin 启动时自动设置为 "plugin"
*/ */
@SpringBootApplication @SpringBootApplication
@EnableAsync @EnableAsync
@@ -23,8 +20,6 @@ public class ServerGuardApplication {
private static volatile ConfigurableApplicationContext context; private static volatile ConfigurableApplicationContext context;
public static void main(String[] args) { public static void main(String[] args) {
// standalone 模式:直接启动
System.setProperty("serverguard.mode", "external");
context = SpringApplication.run(ServerGuardApplication.class, args); context = SpringApplication.run(ServerGuardApplication.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
@@ -34,25 +29,8 @@ public class ServerGuardApplication {
} }
}, "serverguard-shutdown-hook")); }, "serverguard-shutdown-hook"));
logger.info("Aluer ServerGuard standalone mode is ready"); 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"));
/**
* 插件模式启动入口 — 由 AluerPlugin.onEnable() 调用
*
* Paper 插件在 onEnable 时异步调用此方法,
* 返回的 ConfigurableApplicationContext 由插件持有并在 onDisable 时关闭。
*
* @return Spring 应用上下文
*/
public static ConfigurableApplicationContext bootstrap() {
if (context != null) {
logger.warn("Spring Boot context already running, reusing existing context");
return context;
}
context = SpringApplication.run(ServerGuardApplication.class);
logger.info("Aluer ServerGuard plugin mode bootstrapped — Spring context ready");
return context;
} }
public static ConfigurableApplicationContext getContext() { public static ConfigurableApplicationContext getContext() {
@@ -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;
}
}
}
@@ -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("*");
}
}
@@ -1,6 +1,6 @@
package com.aluer.plugin; package com.aluer.plugin;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.InternalCommandExecutor; import com.aluer.plugin.bridge.InternalCommandExecutor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
@@ -9,371 +9,150 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter; import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Aluer 插件命令执行器 — 处理 /aluer /aluerstatus /aluerplayers 等管理命令 * Aluer Agent 命令执行器(精简版)
* *
* 命令列表: * Agent 模式下命令仅作为辅助管理工具,
* /aluer status — 查看防护系统状态 * 封禁/踢人等操作仍通过 InternalCommandExecutor 直接执行。
* /aluer scan <玩家> — 手动触发玩家安全扫描
* /aluer defense — 查看当前防御等级
* /aluer info — 查看系统信息
* /aluerplayers — 查看玩家风险列表
* /aluerblock <玩家|ip> <目标> [理由] — 手动封禁
* /aluerunblock <玩家|ip> <目标> — 手动解封
* /aluerwhitelist <on|off|status> — 白名单管理
*/ */
public class AluerCommandExecutor implements CommandExecutor, TabCompleter { public class AluerCommandExecutor implements CommandExecutor, TabCompleter {
private static final Logger logger = LoggerFactory.getLogger(AluerCommandExecutor.class);
private final AluerPlugin plugin; 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.plugin = plugin;
this.dataBridge = dataBridge; this.wsClient = wsClient;
this.executor = executor;
} }
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
String cmdName = command.getName().toLowerCase(); switch (command.getName().toLowerCase()) {
case "aluer": return handleAluer(sender, args);
switch (cmdName) { case "aluerstatus": return handleStatus(sender);
case "aluer": case "aluerplayers": return handlePlayers(sender);
return handleAluerCommand(sender, args); case "aluerblock": return handleBlock(sender, args);
case "aluerstatus": case "aluerunblock": return handleUnblock(sender, args);
return handleStatusCommand(sender); case "aluerscan": return handleScan(sender, args);
case "aluerplayers": case "aluerwhitelist": return handleWhitelist(sender, args);
return handlePlayersCommand(sender); default: return false;
case "aluerblock":
return handleBlockCommand(sender, args);
case "aluerunblock":
return handleUnblockCommand(sender, args);
case "aluerscan":
return handleScanCommand(sender, args);
case "aluerwhitelist":
return handleWhitelistCommand(sender, args);
default:
return false;
} }
} }
private boolean handleAluerCommand(CommandSender sender, String[] args) { private boolean handleAluer(CommandSender sender, String[] args) {
if (args.length == 0) { if (args.length == 0) { sendHelp(sender); return true; }
sendHelp(sender);
return true;
}
switch (args[0].toLowerCase()) { switch (args[0].toLowerCase()) {
case "status": case "status": return handleStatus(sender);
return handleStatusCommand(sender); case "scan": return handleScan(sender, Arrays.copyOfRange(args, 1, args.length));
case "scan": case "info": return handleInfo(sender);
return handleScanCommand(sender, Arrays.copyOfRange(args, 1, args.length)); default: sendHelp(sender); return true;
case "defense":
return handleDefenseCommand(sender);
case "info":
return handleInfoCommand(sender);
default:
sendHelp(sender);
return true;
} }
} }
private boolean handleStatusCommand(CommandSender sender) { private boolean handleStatus(CommandSender sender) {
sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ Aluer ServerGuard v4.0.0 ═══"); sender.sendMessage(P + ChatColor.YELLOW + "═══ Aluer Agent v5.0.0 ═══");
sender.sendMessage(P + "Server: " + (plugin.isServerConnected() ? ChatColor.GREEN + "已连接" : ChatColor.RED + "未连接"));
if (dataBridge != null) { sender.sendMessage(P + "TPS: " + ChatColor.AQUA + String.format("%.1f", getTPS()));
double tps = dataBridge.getCurrentTps(); sender.sendMessage(P + "在线: " + ChatColor.AQUA + Bukkit.getServer().getOnlinePlayers().size());
int online = dataBridge.getOnlinePlayerCount();
ChatColor tpsColor = tps >= 18 ? ChatColor.GREEN : tps >= 15 ? ChatColor.YELLOW : ChatColor.RED;
sender.sendMessage(PREFIX + "TPS: " + tpsColor + String.format("%.1f", tps) +
ChatColor.RESET + " | 在线玩家: " + ChatColor.AQUA + online);
sender.sendMessage(PREFIX + "Spring 上下文: " +
(plugin.isSpringReady() ? ChatColor.GREEN + "就绪" : ChatColor.RED + "初始化中"));
sender.sendMessage(PREFIX + "包速率: " + ChatColor.GRAY +
String.format("%.0f pps", dataBridge.getPacketsPerSecond()));
sender.sendMessage(PREFIX + "事件处理: " + ChatColor.GRAY +
dataBridge.getEventCount("player.join") + " 登录, " +
dataBridge.getEventCount("chat.message") + " 聊天, " +
dataBridge.getEventCount("block.break") + " 破坏");
} else {
sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪,请稍候...");
}
return true; return true;
} }
private boolean handlePlayersCommand(CommandSender sender) { private boolean handlePlayers(CommandSender sender) {
if (dataBridge == null) { sender.sendMessage(P + ChatColor.YELLOW + "在线玩家: " + Bukkit.getServer().getOnlinePlayers().size());
sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪"); for (Player p : Bukkit.getServer().getOnlinePlayers()) {
return true; 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; return true;
} }
private boolean handleBlockCommand(CommandSender sender, String[] args) { private boolean handleBlock(CommandSender sender, String[] args) {
if (args.length < 2) { if (args.length < 2) { sender.sendMessage(P + ChatColor.RED + "用法: /aluerblock <player|ip> <目标> [理由]"); return true; }
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerblock <player|ip> <目标> [理由]");
return true;
}
String type = args[0].toLowerCase(); String type = args[0].toLowerCase();
String target = args[1]; String target = args[1];
String reason = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) 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);
}
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; return true;
} }
private boolean handleUnblockCommand(CommandSender sender, String[] args) { private boolean handleUnblock(CommandSender sender, String[] args) {
if (args.length < 2) { if (args.length < 2) { sender.sendMessage(P + ChatColor.RED + "用法: /aluerunblock <player|ip> <目标>"); return true; }
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerunblock <player|ip> <目标>");
return true;
}
String type = args[0].toLowerCase(); String type = args[0].toLowerCase();
String target = args[1]; String target = args[1];
boolean ok = "ip".equals(type) ? executor.unbanIP(target) : executor.unbanPlayer(target);
InternalCommandExecutor executor = plugin.getSpringContext() != null sender.sendMessage(P + (ok ? ChatColor.GREEN + "已解封 " : ChatColor.RED + "解封失败 ") + type + ": " + target);
? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
: null;
if (executor == null) {
sender.sendMessage(PREFIX + ChatColor.RED + "命令执行器未就绪");
return true;
}
boolean success;
if ("ip".equals(type)) {
success = executor.unbanIP(target);
} else {
success = executor.unbanPlayer(target);
}
if (success) {
sender.sendMessage(PREFIX + ChatColor.GREEN + "已解封 " + type + ": " + target);
} else {
sender.sendMessage(PREFIX + ChatColor.RED + "解封失败: " + target);
}
return true; return true;
} }
private boolean handleScanCommand(CommandSender sender, String[] args) { private boolean handleScan(CommandSender sender, String[] args) {
if (args.length < 1) { if (args.length < 1) { sender.sendMessage(P + ChatColor.RED + "用法: /aluerscan <玩家名>"); return true; }
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerscan <玩家名>"); Player target = Bukkit.getServer().getPlayer(args[0]);
return true; 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()));
String targetName = args[0]; sender.sendMessage(P + "IP: " + (target.getAddress() != null ? target.getAddress().getAddress().getHostAddress() : "?"));
Player target = Bukkit.getServer().getPlayer(targetName); sender.sendMessage(P + "Ping: " + target.getPing() + "ms | OP: " + target.isOp());
sender.sendMessage(P + "飞行: " + target.isFlying() + " | 滑翔: " + target.isGliding());
if (target == null) {
sender.sendMessage(PREFIX + ChatColor.RED + "玩家不在线: " + targetName);
return true;
}
if (dataBridge == null) {
sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪");
return true;
}
var snap = dataBridge.getPlayer(target.getUniqueId());
if (snap == null) {
sender.sendMessage(PREFIX + ChatColor.RED + "未找到玩家数据");
return true;
}
sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 安全扫描: " + targetName + " ═══");
sender.sendMessage(PREFIX + "位置: " + String.format("(%.1f, %.1f, %.1f) [%s]", snap.x, snap.y, snap.z, snap.world));
sender.sendMessage(PREFIX + "IP: " + snap.ip + " | Ping: " + snap.ping + "ms");
sender.sendMessage(PREFIX + "飞行: " + snap.isFlying + " | 冲刺: " + snap.isSprinting + " | 潜行: " + snap.isSneaking);
sender.sendMessage(PREFIX + "最近攻击目标: " + snap.getRecentTargetCount() + " 个");
sender.sendMessage(PREFIX + "攻击角度一致性: " + (snap.hasSuspiciousAngleConsistency() ? ChatColor.RED + "可疑" : ChatColor.GREEN + "正常"));
sender.sendMessage(PREFIX + "最近消息: " + snap.recentMessages.size() + " 条");
sender.sendMessage(PREFIX + "最近命令: " + snap.recentCommands.size() + " 条");
sender.sendMessage(PREFIX + "在线时长: " + (snap.getOnlineTime() / 60000) + " 分钟");
return true; return true;
} }
private boolean handleDefenseCommand(CommandSender sender) { private boolean handleWhitelist(CommandSender sender, String[] args) {
sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 防御状态 ═══"); if (args.length < 1) { sender.sendMessage(P + ChatColor.RED + "用法: /aluerwhitelist <on|off|status>"); return true; }
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 <on|off|status>");
return true;
}
InternalCommandExecutor executor = plugin.getSpringContext() != null
? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
: null;
switch (args[0].toLowerCase()) { switch (args[0].toLowerCase()) {
case "on": case "on": executor.enableWhitelist(); sender.sendMessage(P + ChatColor.RED + "白名单已启用"); break;
if (executor != null) { case "off": executor.disableWhitelist(); sender.sendMessage(P + ChatColor.GREEN + "白名单已关闭"); break;
executor.enableWhitelist(); case "status": sender.sendMessage(P + "白名单: " + (executor.isWhitelistEnabled() ? ChatColor.RED + "启用" : ChatColor.GREEN + "关闭")); break;
sender.sendMessage(PREFIX + ChatColor.RED + "紧急白名单模式已启用!"); default: sender.sendMessage(P + ChatColor.RED + "用法: /aluerwhitelist <on|off|status>");
}
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 <on|off|status>");
} }
return true; 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) { private void sendHelp(CommandSender sender) {
sender.sendMessage(PREFIX + ChatColor.GOLD + "═══ Aluer ServerGuard 命令 ═══"); sender.sendMessage(P + ChatColor.GOLD + "═══ Aluer Agent 命令 ═══");
sender.sendMessage(ChatColor.YELLOW + "/aluer status" + ChatColor.GRAY + " — 查看防护状态"); sender.sendMessage(ChatColor.YELLOW + "/aluer status" + ChatColor.GRAY + " — 查看Agent状态");
sender.sendMessage(ChatColor.YELLOW + "/aluer scan <玩家>" + ChatColor.GRAY + " — 安全扫描玩家"); sender.sendMessage(ChatColor.YELLOW + "/aluer scan <玩家>" + ChatColor.GRAY + " — 玩家信息");
sender.sendMessage(ChatColor.YELLOW + "/aluer defense" + ChatColor.GRAY + " — 查看防御等级");
sender.sendMessage(ChatColor.YELLOW + "/aluer info" + ChatColor.GRAY + " — 系统信息"); sender.sendMessage(ChatColor.YELLOW + "/aluer info" + ChatColor.GRAY + " — 系统信息");
sender.sendMessage(ChatColor.YELLOW + "/aluerplayers" + ChatColor.GRAY + " — 玩家风险列表"); sender.sendMessage(ChatColor.YELLOW + "/aluerplayers" + ChatColor.GRAY + " — 在线玩家列表");
sender.sendMessage(ChatColor.YELLOW + "/aluerblock <player|ip> <目标>" + ChatColor.GRAY + " — 手动封禁"); sender.sendMessage(ChatColor.YELLOW + "/aluerblock <player|ip> <目标>" + ChatColor.GRAY + " — 封禁");
sender.sendMessage(ChatColor.YELLOW + "/aluerunblock <player|ip> <目标>" + ChatColor.GRAY + " — 手动解封"); sender.sendMessage(ChatColor.YELLOW + "/aluerunblock <player|ip> <目标>" + ChatColor.GRAY + " — 解封");
sender.sendMessage(ChatColor.YELLOW + "/aluerwhitelist <on|off|status>" + ChatColor.GRAY + " — 白名单管理"); sender.sendMessage(ChatColor.YELLOW + "/aluerwhitelist <on|off|status>" + 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 @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
String cmdName = command.getName().toLowerCase(); String cmd = command.getName().toLowerCase();
if ("aluer".equals(cmd) && args.length == 1) return filter(Arrays.asList("status", "scan", "info"), args[0]);
switch (cmdName) { if (("aluerblock".equals(cmd) || "aluerunblock".equals(cmd)) && args.length == 1) return filter(Arrays.asList("player", "ip"), args[0]);
case "aluer": if ("aluerwhitelist".equals(cmd) && args.length == 1) return filter(Arrays.asList("on", "off", "status"), args[0]);
if (args.length == 1) { 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 filterStartsWith(Arrays.asList("status", "scan", "defense", "info"), args[0]);
}
if (args.length == 2 && "scan".equalsIgnoreCase(args[0])) {
return filterStartsWith(
Bukkit.getServer().getOnlinePlayers().stream()
.map(Player::getName)
.collect(Collectors.toList()),
args[1]);
}
break;
case "aluerblock":
if (args.length == 1) {
return filterStartsWith(Arrays.asList("player", "ip"), args[0]);
}
if (args.length == 2 && "player".equalsIgnoreCase(args[0])) {
return filterStartsWith(
Bukkit.getServer().getOnlinePlayers().stream()
.map(Player::getName)
.collect(Collectors.toList()),
args[1]);
}
break;
case "aluerunblock":
if (args.length == 1) {
return filterStartsWith(Arrays.asList("player", "ip"), args[0]);
}
break;
case "aluerwhitelist":
if (args.length == 1) {
return filterStartsWith(Arrays.asList("on", "off", "status"), args[0]);
}
break;
case "aluerscan":
if (args.length == 1) {
return filterStartsWith(
Bukkit.getServer().getOnlinePlayers().stream()
.map(Player::getName)
.collect(Collectors.toList()),
args[0]);
}
break;
}
return Collections.emptyList(); return Collections.emptyList();
} }
private List<String> filterStartsWith(List<String> options, String prefix) { private List<String> filter(List<String> opts, String prefix) {
String lower = prefix.toLowerCase(); return opts.stream().filter(s -> s.toLowerCase().startsWith(prefix.toLowerCase())).collect(Collectors.toList());
return options.stream()
.filter(s -> s.toLowerCase().startsWith(lower))
.collect(Collectors.toList());
} }
} }
+58 -90
View File
@@ -1,152 +1,124 @@
package com.aluer.plugin; package com.aluer.plugin;
import com.aluer.ServerGuardApplication; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.InternalCommandExecutor;
import com.aluer.plugin.listener.*; import com.aluer.plugin.listener.*;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ConfigurableApplicationContext;
/** /**
* Aluer ServerGuard — PaperMC 插件主入口 * Aluer ServerGuard — PaperMC 轻量 Agent 插件
* *
* 插件加载时启动嵌入式 Spring Boot 上下文, * 本插件不嵌入 Spring Boot,而是作为数据采集前端:
* 注册所有 Bukkit 事件监听器,通过 DataBridge * 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 { public class AluerPlugin extends JavaPlugin {
private static final Logger logger = LoggerFactory.getLogger(AluerPlugin.class); private static final Logger logger = LoggerFactory.getLogger(AluerPlugin.class);
/** 全局单例引用,供监听器在 Spring 上下文就绪前获取插件实例 */
private static AluerPlugin instance; private static AluerPlugin instance;
/** Spring Boot 应用上下文,插件关闭时需 gracefully shutdown */ /** WebSocket 客户端,负责与服务端通信 */
private ConfigurableApplicationContext springContext; private AgentWebSocketClient wsClient;
/** 数据桥接中心,连接 Bukkit 事件与安全服务 */ /** Bukkit API 命令执行器 */
private DataBridge dataBridge; private InternalCommandExecutor commandExecutor;
/** Spring 上下文是否已就绪 */ /** 服务端 URL(可通过 config.yml 覆盖) */
private volatile boolean springReady = false; private String serverUrl = "ws://localhost:8080/agent";
// ─── 生命周期 ──────────────────────────────────────── // ─── 生命周期 ────────────────────────────────────────
@Override @Override
public void onLoad() { public void onLoad() {
instance = this; 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 @Override
public void onEnable() { public void onEnable() {
long startMs = System.currentTimeMillis(); long startMs = System.currentTimeMillis();
// 步骤 1:启动嵌入式 Spring Boot(异步,避免阻塞 Paper 主线程) // 读取配置
logger.info("Starting embedded Spring Boot context..."); saveDefaultConfig();
new Thread(() -> { serverUrl = getConfig().getString("server-url", "ws://localhost:8080/agent");
try {
// 设置 Paper 插件模式标志
System.setProperty("serverguard.mode", "plugin");
System.setProperty("server.mode", "plugin");
springContext = ServerGuardApplication.bootstrap(); // 初始化内部命令执行器
dataBridge = springContext.getBean(DataBridge.class); commandExecutor = new InternalCommandExecutor();
springReady = true;
logger.info("Spring Boot context ready — all security services initialized");
} catch (Exception e) {
logger.error("Failed to start Spring Boot context: {}", e.getMessage(), e);
}
}, "aluer-spring-boot").start();
// 步骤 2:等待 Spring 上下文就绪(最长 30 秒) // 初始化 WebSocket 客户端
int waited = 0; wsClient = new AgentWebSocketClient(serverUrl, commandExecutor);
while (!springReady && waited < 30) { wsClient.connect();
try {
Thread.sleep(1000);
waited++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
if (!springReady) { // 注册 Bukkit 事件监听器(传入 WebSocket 客户端)
logger.error("Spring Boot failed to start within 30s — plugin will operate in degraded mode");
}
// 步骤 3:注册 Bukkit 事件监听器
registerEventListeners(); registerEventListeners();
// 步骤 4:注册插件命令 // 注册插件命令
registerCommands(); registerCommands();
long elapsed = System.currentTimeMillis() - startMs; long elapsed = System.currentTimeMillis() - startMs;
logger.info("Aluer ServerGuard enabled in {}ms — real-time protection active", elapsed); logger.info("Agent enabled in {}ms", elapsed);
logger.info(" Listeners: 10 event handlers registered"); logger.info(" Server: {}", serverUrl);
logger.info(" Commands: /aluer, /aluerstatus, /aluerplayers, /aluerblock, /aluerunblock, /aluerscan, /aluerwhitelist"); logger.info(" Listeners: 9 event handlers registered");
logger.info(" Web Console: http://0.0.0.0:8080/"); logger.info(" Commands: /aluer /aluerstatus /aluerplayers /aluerblock /aluerscan /aluerwhitelist");
} }
@Override @Override
public void onDisable() { public void onDisable() {
logger.info("Shutting down Aluer ServerGuard..."); logger.info("Shutting down Aluer Agent...");
// 注销所有 Bukkit 事件监听器
HandlerList.unregisterAll(this); HandlerList.unregisterAll(this);
// 关闭 Spring Boot 上下文 if (wsClient != null) {
if (springContext != null) { wsClient.disconnect();
springContext.close();
springContext = null;
} }
instance = null; instance = null;
logger.info("Aluer ServerGuard disabled — protection stopped"); logger.info("Agent disabled");
} }
// ─── 事件监听器注册 ────────────────────────────────── // ─── 事件监听器注册 ──────────────────────────────────
private void registerEventListeners() { private void registerEventListeners() {
// 注意:监听器在构造函数中接收 DataBridge 引用 Bukkit.getPluginManager().registerEvents(new PlayerEventListener(this, wsClient), this);
// 如果 Spring 未就绪则使用 null(监听器内部做空安全处理) Bukkit.getPluginManager().registerEvents(new CombatEventListener(this, wsClient), this);
DataBridge bridge = springReady ? dataBridge : null; 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")) { 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() { private void registerCommands() {
AluerCommandExecutor executor = new AluerCommandExecutor(this, dataBridge); AluerCommandExecutor executor = new AluerCommandExecutor(this, wsClient, commandExecutor);
var aluerCmd = getCommand("aluer"); var aluerCmd = getCommand("aluer");
if (aluerCmd != null) { if (aluerCmd != null) {
aluerCmd.setExecutor(executor); aluerCmd.setExecutor(executor);
aluerCmd.setTabCompleter(executor); aluerCmd.setTabCompleter(executor);
} }
var statusCmd = getCommand("aluerstatus"); var statusCmd = getCommand("aluerstatus");
if (statusCmd != null) statusCmd.setExecutor(executor); if (statusCmd != null) statusCmd.setExecutor(executor);
var playersCmd = getCommand("aluerplayers"); var playersCmd = getCommand("aluerplayers");
@@ -163,23 +135,19 @@ public class AluerPlugin extends JavaPlugin {
// ─── 公开访问器 ────────────────────────────────────── // ─── 公开访问器 ──────────────────────────────────────
/** 获取插件单例实例 */
public static AluerPlugin getInstance() { public static AluerPlugin getInstance() {
return instance; return instance;
} }
/** 获取 Spring 应用上下文(可能为 null,在 Spring 就绪前) */ public AgentWebSocketClient getWsClient() {
public ConfigurableApplicationContext getSpringContext() { return wsClient;
return springContext;
} }
/** 获取 DataBridge(可能为 null,在 Spring 就绪前) */ public InternalCommandExecutor getCommandExecutor() {
public DataBridge getDataBridge() { return commandExecutor;
return dataBridge;
} }
/** Spring 上下文是否已完全初始化 */ public boolean isServerConnected() {
public boolean isSpringReady() { return wsClient != null && wsClient.isConnected();
return springReady;
} }
} }
@@ -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<String> 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<String> 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<String> 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<WebSocket> 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;
}
}
@@ -1,9 +1,8 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; import com.aluer.plugin.AluerPlugin;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; import com.google.gson.JsonObject;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -17,135 +16,54 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* 方块事件监听器 — 处理方块破坏、放置、告示牌修改 * 方块事件监听器(Agent 端)
*
* 实时检测:
* - Nuker(炸图)— 短时间内破坏大量方块
* - Scaffold(搭桥)— 脚下方块快速连续放置模式
* - SignExploit(告示牌漏洞)— 异常大小的 NBT 标签内容
* - Grief(破坏)— 查找特定贵重方块的大规模破坏模式
*/ */
public class BlockEventListener implements Listener { public class BlockEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(BlockEventListener.class); private static final Logger logger = LoggerFactory.getLogger(BlockEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** Nuker 检测窗口(秒内破坏方块数) */ public BlockEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) {
private static final int NUKER_THRESHOLD = 25;
/** Scaffold 检测窗口(秒内脚下方块放置数) */
private static final int SCAFFOLD_THRESHOLD = 8;
/** 贵重方块列表(Grief 检测目标) */
private static final Material[] VALUABLE_BLOCKS = {
Material.DIAMOND_BLOCK, Material.GOLD_BLOCK, Material.IRON_BLOCK,
Material.EMERALD_BLOCK, Material.NETHERITE_BLOCK, Material.BEACON,
Material.CHEST, Material.ENDER_CHEST, Material.ENCHANTING_TABLE,
Material.ANVIL, Material.DAMAGED_ANVIL, Material.CHIPPED_ANVIL
};
public BlockEventListener(AluerPlugin plugin, DataBridge bridge) {
this.plugin = plugin; this.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) { public void onBlockBreak(BlockBreakEvent event) {
if (bridge == null) return; if (wsClient == null) return;
Player player = event.getPlayer(); Block b = event.getBlock();
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); JsonObject data = new JsonObject();
if (snap == null) return; data.addProperty("playerName", event.getPlayer().getName());
data.addProperty("material", b.getType().name());
Block block = event.getBlock(); data.addProperty("x", b.getX()); data.addProperty("y", b.getY()); data.addProperty("z", b.getZ());
String posKey = String.format("%d,%d,%d", data.addProperty("world", b.getWorld().getName());
block.getX(), block.getY(), block.getZ()); wsClient.sendEvent("BLOCK_BREAK", data);
snap.recentBlockBreaks.add(posKey);
while (snap.recentBlockBreaks.size() > 50) {
snap.recentBlockBreaks.removeFirst();
}
bridge.incrementEvent("block.break");
// —— Nuker 检测 ——
if (snap.recentBlockBreaks.size() >= NUKER_THRESHOLD) {
bridge.alert(AlertType.SECURITY_NUKER,
String.format("玩家 %s 短时间内破坏 %d 个方块,疑似 Nuker",
player.getName(), snap.recentBlockBreaks.size()),
Math.min(0.95, snap.recentBlockBreaks.size() / 40.0),
player.getName());
}
// —— Grief 检测:大量破坏贵重方块 ——
Material type = block.getType();
for (Material valuable : VALUABLE_BLOCKS) {
if (type == valuable) {
bridge.alert(AlertType.SECURITY_GRIEF,
String.format("玩家 %s 破坏贵重方块 %s 位于 %s",
player.getName(), type.name(), posKey),
0.6, player.getName());
break;
}
}
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent event) { public void onBlockPlace(BlockPlaceEvent event) {
if (bridge == null) return; if (wsClient == null) return;
Player player = event.getPlayer(); Block b = event.getBlock();
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); JsonObject data = new JsonObject();
if (snap == null) return; data.addProperty("playerName", event.getPlayer().getName());
data.addProperty("material", b.getType().name());
Block block = event.getBlock(); data.addProperty("x", b.getX()); data.addProperty("y", b.getY()); data.addProperty("z", b.getZ());
String posKey = String.format("%d,%d,%d", data.addProperty("world", b.getWorld().getName());
block.getX(), block.getY(), block.getZ()); wsClient.sendEvent("BLOCK_PLACE", data);
snap.recentBlockPlaces.add(posKey);
while (snap.recentBlockPlaces.size() > 50) {
snap.recentBlockPlaces.removeFirst();
}
bridge.incrementEvent("block.place");
// —— Scaffold 检测:脚下方块快速放置 ——
// 检查是否在玩家脚下方块附近放置
double playerY = snap.y;
if (Math.abs(block.getY() - (playerY - 1)) < 1.5) {
// 脚下方块
int footPlaces = 0;
for (String p : snap.recentBlockPlaces) {
String[] parts = p.split(",");
if (parts.length == 3) {
try {
double by = Double.parseDouble(parts[1]);
if (Math.abs(by - (playerY - 1)) < 1.5) footPlaces++;
} catch (NumberFormatException ignored) {}
}
}
if (footPlaces >= SCAFFOLD_THRESHOLD) {
bridge.alert(AlertType.SECURITY_SCAFFOLD,
String.format("玩家 %s 疑似 Scaffold:快速在脚下方块放置", player.getName()),
0.75, player.getName());
}
}
} }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onSignChange(SignChangeEvent event) { public void onSignChange(SignChangeEvent event) {
if (bridge == null) return; if (wsClient == null) return;
Player player = event.getPlayer(); // —— Agent 端即时拦截:SignExploit ——
// —— SignExploit 检测 ——
// 检测异常长的告示牌文本(可能导致崩溃的 NBT 攻击)
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
String line = event.getLine(i); String line = event.getLine(i);
if (line != null && line.length() > 256) { if (line != null && line.length() > 128) {
bridge.alert(AlertType.SECURITY_SIGN_EXPLOIT,
String.format("玩家 %s 告示牌文本异常长(%d字符),疑似 SignExploit",
player.getName(), line.length()),
0.9, player.getName());
event.setCancelled(true); event.setCancelled(true);
wsClient.sendAlert("SECURITY_SIGN_EXPLOIT",
"即时拦截:玩家 " + event.getPlayer().getName() + " 告示牌文本异常长",
0.9, event.getPlayer().getName());
return; return;
} }
} }
@@ -1,9 +1,8 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; import com.aluer.plugin.AluerPlugin;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; import com.google.gson.JsonObject;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -11,88 +10,84 @@ import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* 聊天事件监听器 — 处理玩家聊天消息 * 聊天事件监听器(Agent 端)
* *
* 实时检测: * Agent 端即时拦截(可取消事件):IP广告/钓鱼链接/刷屏
* - ChatFlood(聊天刷屏)— 短时间内大量消息 * 同时推送所有聊天数据至 ServerGuard 做深度内容分析和模式学习
* - Advertisement(广告)— IP/域名模式匹配
* - PhishingLink(钓鱼链接)— 可疑 URL 检测
* - Profanity(不雅用语)— 关键词过滤
* - Spam(重复消息)— 相同内容重复发送
*/ */
public class ChatEventListener implements Listener { public class ChatEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(ChatEventListener.class); private static final Logger logger = LoggerFactory.getLogger(ChatEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** IP 地址正则 */
private static final Pattern IP_PATTERN = Pattern.compile( private static final Pattern IP_PATTERN = Pattern.compile(
"(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d{1,5})?"); "(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d{1,5})?");
/** URL 正则(含常见短链接服务) */
private static final Pattern URL_PATTERN = Pattern.compile( 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); Pattern.CASE_INSENSITIVE);
/** 聊天刷屏阈值(秒内消息数) */ /** 刷屏检测:最近消息时间戳 */
private static final int SPAM_THRESHOLD = 5; private final Map<UUID, Long> lastChatTime = new ConcurrentHashMap<>();
private static final long SPAM_WINDOW_MS = 10_000; private final Map<UUID, Integer> 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.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onAsyncPlayerChat(AsyncPlayerChatEvent event) { public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
if (bridge == null) return; if (wsClient == null) return;
String message = event.getMessage(); String message = event.getMessage();
String playerName = event.getPlayer().getName(); String playerName = event.getPlayer().getName();
PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId()); UUID uuid = event.getPlayer().getUniqueId();
// —— 聊天刷屏检测 —— // —— Agent 端即时拦截:IP 广告 ——
if (snap != null) { if (IP_PATTERN.matcher(message).find()) {
snap.recentMessages.add(message); event.setCancelled(true);
while (snap.recentMessages.size() > 20) { wsClient.sendAlert("CHAT_ADVERTISEMENT",
snap.recentMessages.removeFirst(); "即时拦截:玩家 " + playerName + " 发送疑似IP地址", 0.9, playerName);
} return;
}
long now = System.currentTimeMillis(); // —— Agent 端即时拦截:钓鱼链接 ——
long recentCount = snap.recentMessages.stream() if (URL_PATTERN.matcher(message).find()) {
.filter(m -> true) // 所有消息都在时间窗口内(Deque 中保留最近 20 条) event.setCancelled(true);
.count(); wsClient.sendAlert("CHAT_PHISHING",
"即时拦截:玩家 " + playerName + " 发送外部链接", 0.85, playerName);
return;
}
if (recentCount >= SPAM_THRESHOLD) { // —— Agent 端即时拦截:刷屏 ——
bridge.alert(AlertType.CHAT_FLOOD, long now = System.currentTimeMillis();
String.format("玩家 %s 聊天刷屏:%d 条消息", playerName, recentCount), Long last = lastChatTime.get(uuid);
0.7, playerName); 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); event.setCancelled(true);
wsClient.sendAlert("CHAT_FLOOD",
"即时拦截:玩家 " + playerName + " 聊天刷屏", 0.8, playerName);
return; return;
} }
} else {
chatBurstCount.put(uuid, 1);
} }
lastChatTime.put(uuid, now);
// —— 广告/IP 检测 —— // 推送聊天数据到 ServerGuard
if (IP_PATTERN.matcher(message).find()) { JsonObject data = new JsonObject();
bridge.alert(AlertType.CHAT_ADVERTISEMENT, data.addProperty("playerName", playerName);
String.format("玩家 %s 发送疑似IP地址:%s", playerName, message), data.addProperty("uuid", uuid.toString());
0.8, playerName); data.addProperty("message", message);
event.setCancelled(true); wsClient.sendEvent("PLAYER_CHAT", data);
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");
} }
} }
@@ -1,112 +1,108 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; import com.aluer.plugin.AluerPlugin;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; import com.google.gson.JsonObject;
import org.bukkit.entity.*; import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.util.Vector;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* 战斗事件监听器 — 处理玩家攻击、实体伤害、死亡事件 * 战斗事件监听器(Agent 端)
* *
* 实时检测: * 采集每次 PvP 攻击的原始数据(双方、距离、角度、伤害值、时间戳),
* - KillAura(杀戮光环)— 多目标快速切换 + 攻击角度一致性 * 推送至 ServerGuard 做 KillAura/Reach/AutoClicker 模式识别。
* - Reach(攻击距离扩展)— 攻击距离超过 3.0 方块
* - AutoClicker(自动连点)— 异常攻击频率
* - ChestSteal — 容器交互跟踪由 InventoryEventListener 处理
*/ */
public class CombatEventListener implements Listener { public class CombatEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(CombatEventListener.class); private static final Logger logger = LoggerFactory.getLogger(CombatEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** 正常最大攻击距离(方块) */ /** Agent 端即时拦截:攻击距离超过此值直接取消(防止极端 Reach hack) */
private static final double MAX_ATTACK_DISTANCE = 3.0; private static final double INSTANT_BLOCK_DISTANCE = 5.0;
/** 攻击距离扩展容忍度 */ /** 记录每个玩家的攻击时间戳(用于 Agent 端简单 CPS 估算) */
private static final double REACH_TOLERANCE = 3.3; private final Map<UUID, Long> lastAttackTime = new ConcurrentHashMap<>();
public CombatEventListener(AluerPlugin plugin, DataBridge bridge) { public CombatEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) {
this.plugin = plugin; this.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) { public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
if (bridge == null) return; if (wsClient == null) return;
// 仅处理玩家攻击
if (!(event.getDamager() instanceof Player attacker)) return; if (!(event.getDamager() instanceof Player attacker)) return;
Entity victim = event.getEntity(); 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()); double distance = attacker.getLocation().distance(victim.getLocation());
attackerSnap.recordAttackDistance(distance); double attackerAngle = Math.toDegrees(Math.atan2(
if (distance > REACH_TOLERANCE) {
bridge.alert(AlertType.SECURITY_REACH,
String.format("玩家 %s 攻击距离异常:%.2f 方块(最大 %.2f)",
attacker.getName(), distance, MAX_ATTACK_DISTANCE),
Math.min(0.95, (distance - MAX_ATTACK_DISTANCE) * 0.5 + 0.5),
attacker.getName());
}
// —— KillAura:攻击角度一致性 ——
Vector attackerDir = attacker.getLocation().getDirection();
double angle = Math.atan2(
victim.getLocation().getZ() - attacker.getLocation().getZ(), victim.getLocation().getZ() - attacker.getLocation().getZ(),
victim.getLocation().getX() - attacker.getLocation().getX() victim.getLocation().getX() - attacker.getLocation().getX()
) * 180.0 / Math.PI; ));
attackerSnap.recordAttackAngle(angle);
if (attackerSnap.hasSuspiciousAngleConsistency()) { // —— Agent 端即时拦截:极端距离直接取消攻击 ——
bridge.alert(AlertType.SECURITY_KILL_AURA, if (distance > INSTANT_BLOCK_DISTANCE) {
String.format("玩家 %s 攻击角度高度一致,疑似 Aimbot", attacker.getName()), event.setCancelled(true);
0.85, attacker.getName()); wsClient.sendAlert("SECURITY_REACH",
"即时拦截:玩家 " + attacker.getName() + " 攻击距离 " + String.format("%.2f", distance) + " 方块",
0.95, attacker.getName());
return;
} }
// —— AutoClicker 检测:攻击频率异常 —— // 计算 CPS(Agent 端简单估算)
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long recentAttacks = attackerSnap.recentTargets.values().stream() Long lastTime = lastAttackTime.get(attacker.getUniqueId());
.filter(t -> t > now - 1000) double estimatedCPS = 0;
.count(); if (lastTime != null && lastTime > 0) {
if (recentAttacks > 20) { // 每秒超过20次攻击(正常CPS上限约14) long interval = now - lastTime;
bridge.alert(AlertType.SECURITY_AUTO_CLICKER, if (interval > 0) estimatedCPS = 1000.0 / interval;
String.format("玩家 %s 攻击频率异常:每秒 %d 次", attacker.getName(), recentAttacks), }
0.8, attacker.getName()); 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) @EventHandler(priority = EventPriority.MONITOR)
public void onEntityDeath(EntityDeathEvent event) { public void onEntityDeath(EntityDeathEvent event) {
if (bridge == null) return; if (wsClient == null) return;
Entity entity = event.getEntity();
Player killer = event.getEntity().getKiller(); Player killer = event.getEntity().getKiller();
if (killer == null) return;
if (killer != null && entity instanceof Player victim) { JsonObject data = new JsonObject();
bridge.incrementEvent("combat.player_kill"); data.addProperty("killerName", killer.getName());
} data.addProperty("victimName", event.getEntity().getName());
data.addProperty("victimType", event.getEntity().getType().name());
wsClient.sendEvent("COMBAT_DEATH", data);
} }
} }
@@ -1,88 +1,57 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; import com.aluer.plugin.AluerPlugin;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; import com.google.gson.JsonObject;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.server.ServerCommandEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* 命令事件监听器 — 监控所有玩家和后台命令执行 * 命令事件监听器(Agent 端)
* *
* 实时检测: * Agent 端即时拦截:命令注入、敏感命令
* - CommandAbuse(命令滥用)— OP 权限的敏感命令使用 * 推送所有命令数据至 ServerGuard 做深度行为分析
* - 插件后门命令 — 异常插件注册的危险命令
* - 命令注入 — 包含 shell 特殊字符的命令
*/ */
public class CommandEventListener implements Listener { public class CommandEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(CommandEventListener.class); private static final Logger logger = LoggerFactory.getLogger(CommandEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** 高危命令前缀列表(需要告警监控) */ private static final String[] BLOCKED_PATTERNS = {
private static final String[] SENSITIVE_COMMANDS = { "${", "`", "&& rm ", "|| rm ", "; rm ", "$((", "eval ", "exec("
"op ", "deop ", "ban ", "pardon ", "whitelist ", "stop", "restart",
"reload", "plugins", "version", "about", "icanhasbukkit",
"save-all", "save-on", "save-off", "difficulty", "defaultgamemode",
"seed", "worldborder", "spreadplayers", "fill", "clone",
"execute", "function", "datapack", "gamerule", "kill",
"summon", "give", "clear", "enchant", "effect",
"tp ", "teleport ", "gamemode ", "kick "
}; };
public CommandEventListener(AluerPlugin plugin, DataBridge bridge) { public CommandEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) {
this.plugin = plugin; this.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
@EventHandler(priority = EventPriority.LOWEST) @EventHandler(priority = EventPriority.LOWEST)
public void onPlayerCommand(PlayerCommandPreprocessEvent event) { public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
if (bridge == null) return; if (wsClient == null) return;
String command = event.getMessage().toLowerCase().trim(); String command = event.getMessage().toLowerCase().trim();
String playerName = event.getPlayer().getName(); String playerName = event.getPlayer().getName();
PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId());
bridge.incrementEvent("command.player"); // —— Agent 端即时拦截:命令注入 ——
for (String pattern : BLOCKED_PATTERNS) {
// 记录最近命令(用于滥用检测) if (command.contains(pattern)) {
if (snap != null) { event.setCancelled(true);
snap.recentCommands.add(command); wsClient.sendAlert("COMMAND_ABUSE",
while (snap.recentCommands.size() > 10) { "即时拦截:玩家 " + playerName + " 命令注入尝试", 0.95, playerName);
snap.recentCommands.removeFirst(); return;
} }
} }
// —— 敏感命令监控 —— // 推送命令数据到 ServerGuard
for (String sensitive : SENSITIVE_COMMANDS) { JsonObject data = new JsonObject();
if (command.startsWith("/" + sensitive) || command.startsWith("minecraft:" + sensitive)) { data.addProperty("playerName", playerName);
bridge.alert(AlertType.COMMAND_ABUSE, data.addProperty("command", event.getMessage());
String.format("玩家 %s 执行敏感命令:%s", playerName, event.getMessage()), data.addProperty("isOp", event.getPlayer().isOp());
0.6, playerName); wsClient.sendEvent("PLAYER_COMMAND", data);
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");
} }
} }
@@ -1,83 +1,57 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; 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.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; 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.bukkit.event.player.PlayerFishEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/**
* 实体事件监听器 — 处理实体生成、拾取、交互
*
* 实时检测:
* - AutoFish(自动钓鱼)— 异常快速的钓鱼收杆时机
* - 实体生成速率异常(可能为故意的服务器压力测试)
*/
public class EntityEventListener implements Listener { public class EntityEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(EntityEventListener.class); private static final Logger logger = LoggerFactory.getLogger(EntityEventListener.class);
private final AluerPlugin plugin; 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.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR)
public void onEntitySpawn(EntitySpawnEvent event) { public void onEntitySpawn(EntitySpawnEvent event) {
if (bridge == null) return; if (wsClient == null) return;
bridge.incrementEvent("entity.spawn"); 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) { public void onPlayerFish(PlayerFishEvent event) {
if (bridge == null) return; if (wsClient == null) return;
Player player = event.getPlayer(); if (event.getState() == PlayerFishEvent.State.CAUGHT_FISH || event.getState() == PlayerFishEvent.State.CAUGHT_ENTITY) {
JsonObject data = new JsonObject();
// —— AutoFish 检测 —— data.addProperty("playerName", event.getPlayer().getName());
// 正常钓鱼需要玩家注意力响应,AutoFish hack 会在鱼咬钩瞬间自动收杆 data.addProperty("state", event.getState().name());
if (event.getState() == PlayerFishEvent.State.CAUGHT_FISH wsClient.sendEvent("PLAYER_FISH", data);
|| event.getState() == PlayerFishEvent.State.CAUGHT_ENTITY) {
bridge.incrementEvent("fish.caught");
// 记录钓鱼收杆时间,如果连续快速收杆则标记
var snap = bridge.getPlayer(player.getUniqueId());
if (snap != null) {
long now = System.currentTimeMillis();
long recentCatches = snap.recentTargets.values().stream()
.filter(t -> t > now - 30_000) // 30秒内
.count();
if (recentCatches > 5) { // 30秒内收杆超过5次(正常钓鱼约15-60秒一次)
bridge.alert(AlertType.SECURITY_AUTO_FISH,
String.format("玩家 %s 30秒内钓鱼收杆 %d 次,疑似 AutoFish",
player.getName(), recentCatches),
0.7, player.getName());
}
// 复用 recentTargets 的 key 为 "fish" 来记录钓鱼时间
snap.recentTargets.put("fish:" + System.nanoTime(), now);
}
} }
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onEntityPickupItem(EntityPickupItemEvent event) { public void onEntityPickup(EntityPickupItemEvent event) {
if (bridge == null) return; if (wsClient == null) return;
if (event.getEntity() instanceof Player) { if (event.getEntity() instanceof Player p) {
bridge.incrementEvent("entity.pickup"); 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");
}
} }
@@ -1,116 +1,49 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; import com.aluer.plugin.AluerPlugin;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; import com.google.gson.JsonObject;
import org.bukkit.block.Container;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.inventory.*; import org.bukkit.event.inventory.*;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/**
* 物品栏事件监听器 — 处理容器交互、物品移动
*
* 实时检测:
* - ChestSteal(快速偷箱)— 短时间内从容器取出大量物品
* - InventoryManipulation(物品栏操控)— 异常快速的物品移动模式
* - Dupe(物品复制)— 异常物品数量变化
*/
public class InventoryEventListener implements Listener { public class InventoryEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(InventoryEventListener.class); private static final Logger logger = LoggerFactory.getLogger(InventoryEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** 单次 Click 事件中物品数量变化的阈值(超过视为可疑) */ public InventoryEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) {
private static final int ITEM_CHANGE_THRESHOLD = 64;
public InventoryEventListener(AluerPlugin plugin, DataBridge bridge) {
this.plugin = plugin; this.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onInventoryClick(InventoryClickEvent event) { public void onInventoryClick(InventoryClickEvent event) {
if (bridge == null) return; if (wsClient == null) return;
if (!(event.getWhoClicked() instanceof Player player)) return; if (!(event.getWhoClicked() instanceof Player p)) return;
JsonObject data = new JsonObject();
Inventory inv = event.getInventory(); data.addProperty("playerName", p.getName());
InventoryType type = inv.getType(); data.addProperty("inventoryType", event.getInventory().getType().name());
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); data.addProperty("action", event.getAction().name());
if (snap == null) return; data.addProperty("slot", event.getSlot());
data.addProperty("currentItem", event.getCurrentItem() != null ? event.getCurrentItem().getType().name() : "AIR");
bridge.incrementEvent("inventory.click"); data.addProperty("cursorItem", event.getCursor() != null ? event.getCursor().getType().name() : "AIR");
wsClient.sendEvent("INVENTORY_CLICK", data);
// —— ChestSteal 检测 ——
// 检测玩家在短时间内从他人容器中快速取走物品
if (type == InventoryType.CHEST || type == InventoryType.BARREL
|| type == InventoryType.SHULKER_BOX || type == InventoryType.ENDER_CHEST) {
ItemStack current = event.getCurrentItem();
ItemStack cursor = event.getCursor();
// 快速移动(Shift-Click)整组物品
if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY && current != null) {
int movedAmount = current.getAmount();
if (movedAmount >= ITEM_CHANGE_THRESHOLD / 2) {
bridge.alert(AlertType.SECURITY_CHEST_STEAL,
String.format("玩家 %s 从容器快速取走 %d 个物品",
player.getName(), movedAmount),
0.5, player.getName());
}
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onInventoryOpen(InventoryOpenEvent event) {
if (bridge == null) return;
if (!(event.getPlayer() instanceof Player player)) return;
bridge.incrementEvent("inventory.open");
// 记录容器打开以辅助 ChestSteal 上下文判断
Inventory inv = event.getInventory();
if (inv.getHolder() instanceof Container container) {
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
if (snap != null) {
// 记录容器位置用于后续分析
String locKey = String.format("container:%d,%d,%d",
container.getLocation().getBlockX(),
container.getLocation().getBlockY(),
container.getLocation().getBlockZ());
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onInventoryClose(InventoryCloseEvent event) {
if (bridge == null) return;
if (!(event.getPlayer() instanceof Player)) return;
bridge.incrementEvent("inventory.close");
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onInventoryDrag(InventoryDragEvent event) { public void onInventoryDrag(InventoryDragEvent event) {
if (bridge == null) return; if (wsClient == null) return;
if (!(event.getWhoClicked() instanceof Player player)) return; if (!(event.getWhoClicked() instanceof Player p)) return;
bridge.incrementEvent("inventory.drag"); JsonObject data = new JsonObject();
data.addProperty("playerName", p.getName());
// 大量物品拖动(可能为 InventoryManipulation hack) data.addProperty("slotsCount", event.getInventorySlots().size());
int totalSlots = event.getInventorySlots().size(); wsClient.sendEvent("INVENTORY_DRAG", data);
if (totalSlots > 16) {
bridge.alert(AlertType.SECURITY_INVENTORY_MANIPULATION,
String.format("玩家 %s 一次拖动覆盖 %d 个物品栏位,疑似 InventoryManipulation",
player.getName(), totalSlots),
0.65, player.getName());
}
} }
} }
@@ -1,8 +1,9 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; 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.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -10,50 +11,52 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* 包级事件监听器 — 利用 Paper API 对网络包进行细粒度监控 * 包级事件监听器(Agent 端,Paper 特定 API)
* *
* Paper 特有 API,非 Paper 服务端会自动跳过注册。 * 利用 Paper 特有 API 监控网络包速率,
* * 非 Paper 服务端自动跳过注册。
* 实时检测:
* - PacketFlood(包洪水)— 单个玩家发送异常数量的包
* - TabCompleteCrash — 异常大的 TabComplete 请求
* - BookBan — 超大书内容导致的崩溃包
* - ResourcePackExploit — 恶意资源包下载尝试
*/ */
public class PacketEventListener implements Listener { public class PacketEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(PacketEventListener.class); private static final Logger logger = LoggerFactory.getLogger(PacketEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** 每秒最大允许的包速率(单玩家) */ private long lastPacketReport = System.currentTimeMillis();
private static final int MAX_PACKETS_PER_SECOND = 200; 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.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
/** /**
* 监听 Paper 的包预接收事件(需 Paper 1.20.6+) * 定期上报包速率指标(由 ServerGuard 端调度器触发的心跳也会附带此数据)
* 使用反射调用避免编译时硬依赖 Paper 特定 API 类
*/ */
@EventHandler(priority = EventPriority.LOWEST) public void reportPacketRate() {
public void onPacketReceive(com.destroystokyo.paper.event.player.PlayerHandshakeEvent event) { if (wsClient == null) return;
if (bridge == null) return; long now = System.currentTimeMillis();
// 握手阶段即可获取连接来源 long elapsed = now - lastPacketReport;
bridge.incrementEvent("packet.handshake"); 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;
} }
/** private double getTPS() {
* 通用的包事件监听 — 使用 Paper 1.21+ 的 AsyncPacketReceiveEvent try {
* 注:此类在 paper-api 1.21 中可用,但具体 API 可能因版本而异 double[] tps = Bukkit.getServer().getTPS();
*/ return tps.length > 0 ? tps[0] : 20.0;
// @EventHandler 注释留空,待 Paper 版本确认后启用 } catch (Exception e) {
// 当前通过 DataBridge.recordPacket() 提供包速率追踪入口 return 20.0;
public void onAsyncPacket(Object event) { }
if (bridge == null) return;
bridge.recordPacket();
bridge.incrementEvent("packet.total");
} }
} }
@@ -1,9 +1,8 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.model.AlertType;
import com.aluer.plugin.AluerPlugin; import com.aluer.plugin.AluerPlugin;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.plugin.bridge.AgentWebSocketClient;
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot; import com.google.gson.JsonObject;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@@ -14,171 +13,141 @@ import org.bukkit.event.player.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* 玩家事件监听器 — 处理登录、退出、移动、传送、死亡等核心玩家事件 * 玩家事件监听器(Agent 端)
* *
* 实时检测: * 采集:登录/退出/移动/传送/摔伤等原始事件,
* - Speed(移动速度异常)— 水平速度超过 0.65 blocks/tick 则标记 * 序列化为 JSON 后通过 WebSocket 推送至外部 ServerGuard 引擎。
* - Fly(非法飞行)— 非创造/旁观模式下,垂直位移模式异常 *
* - Jesus(水行)— 连续在水中但 y 坐标不下降 * Agent 端仅做数据采集和即时拦截(聊天/命令等可取消事件),
* - NoFall(无摔伤)— 从高处落地无伤害 * 深度分析(Speed/Fly/Jesus/NoFall 模式识别)由 ServerGuard 端完成。
*/ */
public class PlayerEventListener implements Listener { public class PlayerEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(PlayerEventListener.class); private static final Logger logger = LoggerFactory.getLogger(PlayerEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** 最大允许水平速度(blocks/tick),超过此值视为 Speed Hack */ /** 用于 Agent 端简单阈值拦截(阻塞即时作弊,深度分析仍由 Server 端处理) */
private static final double MAX_HORIZONTAL_SPEED = 0.65; private static final double MAX_INSTANT_SPEED = 1.2; // blocks/tick,超高值直接拦截
/** 可疑移动的告警置信度阈值 */ /** 玩家上次位置记录(用于计算移动距离) */
private static final double SUSPICIOUS_CONFIDENCE = 0.75; private final Map<UUID, Location> lastLocations = new ConcurrentHashMap<>();
public PlayerEventListener(AluerPlugin plugin, DataBridge bridge) { public PlayerEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) {
this.plugin = plugin; this.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
// ─── 登录/退出 ──────────────────────────────────────
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
if (bridge == null) return; if (wsClient == null) return;
Player player = event.getPlayer(); Player p = event.getPlayer();
bridge.registerPlayer(player); JsonObject data = new JsonObject();
bridge.incrementEvent("player.join"); data.addProperty("playerName", p.getName());
data.addProperty("uuid", p.getUniqueId().toString());
// 检查同 IP 多账号 data.addProperty("ip", p.getAddress() != null && p.getAddress().getAddress() != null
String ip = player.getAddress() != null && player.getAddress().getAddress() != null ? p.getAddress().getAddress().getHostAddress() : "unknown");
? player.getAddress().getAddress().getHostAddress() data.addProperty("world", p.getWorld().getName());
: "unknown"; data.addProperty("x", p.getLocation().getX());
var sameIP = bridge.getPlayersByIP(ip); data.addProperty("y", p.getLocation().getY());
if (sameIP.size() > 2) { data.addProperty("z", p.getLocation().getZ());
bridge.alert(AlertType.SECURITY_ALT_ACCOUNT, data.addProperty("isOp", p.isOp());
String.format("玩家 %s 使用IP %s 登录,该IP已有 %d 个在线账号", data.addProperty("ping", p.getPing());
player.getName(), ip, sameIP.size()), wsClient.sendEvent("PLAYER_JOIN", data);
0.7, player.getName());
}
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) { public void onPlayerQuit(PlayerQuitEvent event) {
if (bridge == null) return; if (wsClient == null) return;
bridge.removePlayer(event.getPlayer().getUniqueId()); Player p = event.getPlayer();
bridge.incrementEvent("player.quit"); 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) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(PlayerMoveEvent event) { public void onPlayerMove(PlayerMoveEvent event) {
if (bridge == null) return; if (wsClient == null) return;
Player player = event.getPlayer(); Player p = event.getPlayer();
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
if (snap == null) return;
Location from = event.getFrom(); Location from = event.getFrom();
Location to = event.getTo(); 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 dx = to.getX() - from.getX();
double dz = to.getZ() - from.getZ(); double dz = to.getZ() - from.getZ();
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
double dy = to.getY() - from.getY(); double dy = to.getY() - from.getY();
double horizontal = Math.sqrt(dx * dx + dz * dz);
snap.totalDistanceMoved += horizontalDist; // —— Agent 端即时拦截:极高速度直接阻止移动 ——
if (horizontal > MAX_INSTANT_SPEED && p.getGameMode() == org.bukkit.GameMode.SURVIVAL
// —— Speed 检测 —— && !p.isGliding() && !p.isInsideVehicle()) {
// 排除创造/旁观模式、鞘翅飞行、骑乘状态 event.setCancelled(true);
if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL wsClient.sendAlert("SECURITY_SPEED",
&& !player.isGliding() "即时拦截:玩家 " + p.getName() + " 水平速度 " + String.format("%.3f", horizontal) + " blocks/tick",
&& !player.isInsideVehicle() 0.95, p.getName());
&& !player.isFlying()) { return;
if (horizontalDist > MAX_HORIZONTAL_SPEED) {
bridge.alert(AlertType.SECURITY_SPEED,
String.format("玩家 %s 移动速度异常:水平 %.3f blocks/tick(阈值 %.2f)",
player.getName(), horizontalDist, MAX_HORIZONTAL_SPEED),
Math.min(0.95, horizontalDist / MAX_HORIZONTAL_SPEED * 0.6),
player.getName());
}
} }
// —— Fly 检测 —— // 推送移动数据到 ServerGuard 做深度分析
// 非飞行玩家在空中时持续 y 坐标下降过慢 JsonObject data = new JsonObject();
if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL data.addProperty("playerName", p.getName());
&& !player.isFlying() data.addProperty("uuid", p.getUniqueId().toString());
&& !player.isGliding() data.addProperty("fromX", from.getX()); data.addProperty("fromY", from.getY()); data.addProperty("fromZ", from.getZ());
&& !player.isInWater() data.addProperty("toX", to.getX()); data.addProperty("toY", to.getY()); data.addProperty("toZ", to.getZ());
&& !player.isInsideVehicle()) { 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 不下降反而上升 lastLocations.put(p.getUniqueId(), to.clone());
boolean onGround = ((org.bukkit.entity.LivingEntity) player).isOnGround();
if (!onGround && dy > 0.05 && horizontalDist < 0.1) {
bridge.alert(AlertType.SECURITY_FLY,
String.format("玩家 %s 疑似飞行:空中上升 dy=%.3f", player.getName(), dy),
SUSPICIOUS_CONFIDENCE, player.getName());
}
}
// —— Jesus(水行)检测 ——
if (player.isInWater() && player.getGameMode() == org.bukkit.GameMode.SURVIVAL
&& !player.isFlying() && !player.isSwimming()) {
// 在水中但 y 坐标超过水面特征线
if (dy > -0.01 && horizontalDist > 0.3) {
bridge.alert(AlertType.SECURITY_JESUS,
String.format("玩家 %s 疑似水行:水中水平移动 %.3f blocks", player.getName(), horizontalDist),
0.65, player.getName());
}
}
} }
// ─── 传送跟踪 ──────────────────────────────────────
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerTeleport(PlayerTeleportEvent event) { public void onPlayerTeleport(PlayerTeleportEvent event) {
if (bridge == null) return; if (wsClient == null || event.getTo() == null) return;
PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId()); JsonObject data = new JsonObject();
if (snap != null && event.getTo() != null) { data.addProperty("playerName", event.getPlayer().getName());
snap.updatePosition(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(), data.addProperty("fromX", event.getFrom().getX()); data.addProperty("fromY", event.getFrom().getY()); data.addProperty("fromZ", event.getFrom().getZ());
event.getTo().getYaw(), event.getTo().getPitch()); 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) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerDamage(EntityDamageEvent event) { public void onPlayerDamage(EntityDamageEvent event) {
if (bridge == null) return; if (wsClient == null) return;
if (!(event.getEntity() instanceof Player player)) return; if (!(event.getEntity() instanceof Player p)) return;
if (event.getCause() != EntityDamageEvent.DamageCause.FALL) return;
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId()); JsonObject data = new JsonObject();
if (snap == null) return; 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(); if (event.getCause() == EntityDamageEvent.DamageCause.FALL) {
// 正常情况:掉落 > 3 方块高度应有伤害 wsClient.sendEvent("PLAYER_DAMAGE", data);
// NoFall hack 会让服务端认为掉落距离为 0
if (fallDistance > 3.5 && event.getDamage() < 1.0) { // Agent 端即时 NoFall 拦截:高坠落低伤害直接标记
bridge.alert(AlertType.SECURITY_NOFALL, if (p.getFallDistance() > 4.0 && event.getDamage() < 1.0) {
String.format("玩家 %s 疑似 NoFall:掉落 %.1f 方块但伤害仅 %.1f", wsClient.sendAlert("SECURITY_NOFALL",
player.getName(), fallDistance, event.getDamage()), "玩家 " + p.getName() + " 坠落 " + String.format("%.1f", p.getFallDistance()) + " 方块但伤害仅 " + String.format("%.1f", event.getDamage()),
0.8, player.getName()); 0.85, p.getName());
}
} }
} }
} }
@@ -1,58 +1,45 @@
package com.aluer.plugin.listener; package com.aluer.plugin.listener;
import com.aluer.plugin.AluerPlugin; 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.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldSaveEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/**
* 世界事件监听器 — 处理区块加载/卸载、世界保存
*
* 主要用于性能监控:
* - 区块加载速率(异常高可能表明 Xray 或 Baritone 矿透机器人)
* - 世界保存耗时跟踪
*/
public class WorldEventListener implements Listener { public class WorldEventListener implements Listener {
private static final Logger logger = LoggerFactory.getLogger(WorldEventListener.class); private static final Logger logger = LoggerFactory.getLogger(WorldEventListener.class);
private final AluerPlugin plugin; private final AluerPlugin plugin;
private final DataBridge bridge; private final AgentWebSocketClient wsClient;
/** 区块加载速率告警阈值(每秒区块数),Baritone/Xray 会快速加载大量区块 */ public WorldEventListener(AluerPlugin plugin, AgentWebSocketClient wsClient) {
private static final int CHUNK_LOAD_RATE_WARNING = 30;
public WorldEventListener(AluerPlugin plugin, DataBridge bridge) {
this.plugin = plugin; this.plugin = plugin;
this.bridge = bridge; this.wsClient = wsClient;
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onChunkLoad(ChunkLoadEvent event) { public void onChunkLoad(ChunkLoadEvent event) {
if (bridge == null) return; if (wsClient == null) return;
long count = bridge.incrementEvent("world.chunk_load"); JsonObject data = new JsonObject();
data.addProperty("world", event.getWorld().getName());
// 高频区块加载可能表明 Baritone 矿透或 Xray 探索 data.addProperty("chunkX", event.getChunk().getX());
if (count > CHUNK_LOAD_RATE_WARNING) { data.addProperty("chunkZ", event.getChunk().getZ());
// 每秒重置计数器(由 DataBridge 的调度处理) data.addProperty("isNew", event.isNewChunk());
} wsClient.sendEvent("CHUNK_LOAD", data);
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onChunkUnload(ChunkUnloadEvent event) { public void onChunkUnload(ChunkUnloadEvent event) {
if (bridge == null) return; if (wsClient == null) return;
bridge.incrementEvent("world.chunk_unload"); JsonObject data = new JsonObject();
} data.addProperty("world", event.getWorld().getName());
data.addProperty("chunkX", event.getChunk().getX());
@EventHandler(priority = EventPriority.MONITOR) data.addProperty("chunkZ", event.getChunk().getZ());
public void onWorldSave(WorldSaveEvent event) { wsClient.sendEvent("CHUNK_UNLOAD", data);
if (bridge == null) return;
bridge.incrementEvent("world.save");
logger.debug("World saved: {}", event.getWorld().getName());
} }
} }
@@ -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<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
/** Agent 元数据(agentId → 握手信息) */
private final Map<String, JsonObject> agentMetadata = new ConcurrentHashMap<>();
/** 事件消息处理器(由 ServerGuardService 注入) */
private volatile BiConsumer<String, JsonObject> eventHandler;
/** 指标消息处理器 */
private volatile BiConsumer<String, JsonObject> metricsHandler;
/** 告警消息处理器 */
private volatile BiConsumer<String, JsonObject> alertHandler;
/** 命令执行结果处理器 */
private volatile BiConsumer<String, JsonObject> 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<String, JsonObject> handler) {
this.eventHandler = handler;
}
public void setMetricsHandler(BiConsumer<String, JsonObject> handler) {
this.metricsHandler = handler;
}
public void setAlertHandler(BiConsumer<String, JsonObject> handler) {
this.alertHandler = handler;
}
public void setCommandResultHandler(BiConsumer<String, JsonObject> 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;
}
}
@@ -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);
}
}
}
@@ -3,11 +3,13 @@ package com.aluer.service;
import com.aluer.ai.*; import com.aluer.ai.*;
import com.aluer.alert.EmailAlertService; import com.aluer.alert.EmailAlertService;
import com.aluer.config.ServerGuardConfig; import com.aluer.config.ServerGuardConfig;
import com.aluer.monitor.*;
import com.aluer.model.AlertEvent; import com.aluer.model.AlertEvent;
import com.aluer.model.AlertType;
import com.aluer.model.MetricsData; import com.aluer.model.MetricsData;
import com.aluer.plugin.bridge.DataBridge; import com.aluer.monitor.ProcessMonitor;
import com.aluer.plugin.bridge.InternalCommandExecutor; import com.aluer.monitor.ResourceMonitor;
import com.aluer.server.AgentWebSocketServer;
import com.google.gson.JsonObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.CommandLineRunner;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/**
* ServerGuard 核心调度服务
*
* 负责:
* 1. 外部监控模式:进程保活 + 资源监控 + 日志分析(传统 external mode)
* 2. Agent 模式:接收 Paper 插件 Agent 通过 WebSocket 推送的实时事件/指标/告警
* 3. 告警处理管道(AI 分析 + 自动执行 + 邮件通知),两种模式共用
*/
@Service @Service
public class ServerGuardService implements CommandLineRunner { public class ServerGuardService implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(ServerGuardService.class); private static final Logger logger = LoggerFactory.getLogger(ServerGuardService.class);
@@ -28,8 +39,6 @@ public class ServerGuardService implements CommandLineRunner {
private final ServerGuardConfig config; private final ServerGuardConfig config;
private final ProcessMonitor processMonitor; private final ProcessMonitor processMonitor;
private final ResourceMonitor resourceMonitor; private final ResourceMonitor resourceMonitor;
private final ConnectionMonitor connectionMonitor;
private final LogMonitor logMonitor;
private final AdaptiveThreshold adaptiveThreshold; private final AdaptiveThreshold adaptiveThreshold;
private final AnomalyDetector anomalyDetector; private final AnomalyDetector anomalyDetector;
private final TimeSeriesPredictor timeSeriesPredictor; private final TimeSeriesPredictor timeSeriesPredictor;
@@ -37,15 +46,14 @@ public class ServerGuardService implements CommandLineRunner {
private final EmailAlertService emailAlertService; private final EmailAlertService emailAlertService;
private final DeepSeekClient deepSeekClient; private final DeepSeekClient deepSeekClient;
private final AutoExecutor autoExecutor; private final AutoExecutor autoExecutor;
private final RconClient rconClient;
/** 插件模式下的数据桥接(external 模式下为 null) */ /** Agent WebSocket 服务器(接收 Paper 插件 Agent 的实时数据推送) */
@Autowired(required = false) @Autowired(required = false)
private DataBridge dataBridge; private AgentWebSocketServer agentServer;
/** 插件模式下的内部命令执行器(external 模式下为 null) */ /** Agent 命令调度器(通过 WebSocket 向 Agent 下发防御指令) */
@Autowired(required = false) @Autowired(required = false)
private InternalCommandExecutor internalCommandExecutor; private AgentCommandDispatcher agentCommandDispatcher;
@Value("${server.port:8080}") @Value("${server.port:8080}")
private int serverPort; private int serverPort;
@@ -53,25 +61,23 @@ public class ServerGuardService implements CommandLineRunner {
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private final AtomicBoolean running = new AtomicBoolean(true); private final AtomicBoolean running = new AtomicBoolean(true);
/** Agent 推送的事件/告警计数 */
private final AtomicBoolean agentHandlersRegistered = new AtomicBoolean(false);
public ServerGuardService( public ServerGuardService(
ServerGuardConfig config, ServerGuardConfig config,
ProcessMonitor processMonitor, ProcessMonitor processMonitor,
ResourceMonitor resourceMonitor, ResourceMonitor resourceMonitor,
ConnectionMonitor connectionMonitor,
LogMonitor logMonitor,
AdaptiveThreshold adaptiveThreshold, AdaptiveThreshold adaptiveThreshold,
AnomalyDetector anomalyDetector, AnomalyDetector anomalyDetector,
TimeSeriesPredictor timeSeriesPredictor, TimeSeriesPredictor timeSeriesPredictor,
AttackDetector attackDetector, AttackDetector attackDetector,
EmailAlertService emailAlertService, EmailAlertService emailAlertService,
DeepSeekClient deepSeekClient, DeepSeekClient deepSeekClient,
AutoExecutor autoExecutor, AutoExecutor autoExecutor) {
RconClient rconClient) {
this.config = config; this.config = config;
this.processMonitor = processMonitor; this.processMonitor = processMonitor;
this.resourceMonitor = resourceMonitor; this.resourceMonitor = resourceMonitor;
this.connectionMonitor = connectionMonitor;
this.logMonitor = logMonitor;
this.adaptiveThreshold = adaptiveThreshold; this.adaptiveThreshold = adaptiveThreshold;
this.anomalyDetector = anomalyDetector; this.anomalyDetector = anomalyDetector;
this.timeSeriesPredictor = timeSeriesPredictor; this.timeSeriesPredictor = timeSeriesPredictor;
@@ -79,7 +85,6 @@ public class ServerGuardService implements CommandLineRunner {
this.emailAlertService = emailAlertService; this.emailAlertService = emailAlertService;
this.deepSeekClient = deepSeekClient; this.deepSeekClient = deepSeekClient;
this.autoExecutor = autoExecutor; this.autoExecutor = autoExecutor;
this.rconClient = rconClient;
this.scheduler = Executors.newScheduledThreadPool(4, r -> { this.scheduler = Executors.newScheduledThreadPool(4, r -> {
Thread t = new Thread(r, "serverguard-scheduler"); Thread t = new Thread(r, "serverguard-scheduler");
@@ -93,114 +98,134 @@ public class ServerGuardService implements CommandLineRunner {
printBanner(); printBanner();
validateConfiguration(); validateConfiguration();
startMonitoring(); // 注册 Agent WebSocket 事件处理器
if (agentServer != null) {
// 插件模式:注册 DataBridge 告警/指标处理器 registerAgentHandlers();
if (config.isPluginMode() && dataBridge != null) { agentHandlersRegistered.set(true);
dataBridge.setAlertHandler(this::handleAlert); logger.info("Agent WebSocket handlers registered — ready to receive agent data");
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");
} }
// 启动监控(external 模式使用传统监控,agent 模式使用 agent 推送 + 传统指标补充)
startMonitoring();
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Shutdown signal received, gracefully stopping..."); logger.info("Shutdown signal received, gracefully stopping...");
running.set(false); running.set(false);
scheduler.shutdown(); scheduler.shutdown();
try { try {
if (!scheduler.awaitTermination(8, TimeUnit.SECONDS)) { if (!scheduler.awaitTermination(8, TimeUnit.SECONDS)) scheduler.shutdownNow();
scheduler.shutdownNow();
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
scheduler.shutdownNow(); scheduler.shutdownNow();
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
emailAlertService.shutdown(); emailAlertService.shutdown();
if (rconClient.isConnected()) {
rconClient.close();
}
logger.info("Aluer ServerGuard stopped"); logger.info("Aluer ServerGuard stopped");
}, "serverguard-shutdown")); }, "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() { private void printBanner() {
boolean hasAgent = agentServer != null;
logger.info(""); logger.info("");
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("║ 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("╠══════════════════════════════════════════════════════════════╣");
logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version"))); logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version")));
logger.info("║ Available CPUs: {}{} ║", logger.info("║ Web Console: http://0.0.0.0:{}{} ║", serverPort, " ");
String.format("%-32s", Runtime.getRuntime().availableProcessors()), if (hasAgent) {
""); logger.info("║ Agent WS: ws://0.0.0.0:{}/agent{} ║", serverPort, " ");
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("║ DeepSeek AI: {}{} ║", logger.info("║ DeepSeek AI: {}{} ║",
deepSeekClient.isEnabled() ? "enabled (" + config.getAi().getDeepseek().getModel() + ")" : "disabled", deepSeekClient.isEnabled() ? "enabled (" + config.getAi().getDeepseek().getModel() + ")" : "disabled",
deepSeekClient.isEnabled() ? "" : " "); 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(""); 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() { private void validateConfiguration() {
int warnings = 0; int warnings = 0;
if (!deepSeekClient.isEnabled()) { if (!deepSeekClient.isEnabled()) {
logger.warn("CONFIG: DeepSeek AI is disabled. Set DEEPSEEK_API_KEY to enable AI features."); logger.warn("CONFIG: DeepSeek AI is disabled. Set DEEPSEEK_API_KEY to enable AI features.");
warnings++; 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) { if (warnings > 0) {
logger.info("Configuration check complete: {} warning(s) found.", warnings); logger.info("Configuration check complete: {} warning(s) found.", warnings);
} else { } else {
@@ -208,27 +233,30 @@ public class ServerGuardService implements CommandLineRunner {
} }
} }
// ─── 监控循环 ──────────────────────────────────────
private void startMonitoring() { private void startMonitoring() {
int intervalSeconds = config.getMinecraft().getCheckIntervalSeconds(); int interval = config.getMinecraft().getCheckIntervalSeconds();
scheduler.scheduleAtFixedRate(this::checkProcess, 0, intervalSeconds, TimeUnit.SECONDS); // 进程保活(external 模式使用)
scheduler.scheduleAtFixedRate(this::collectAndAnalyzeMetrics, 5, intervalSeconds, TimeUnit.SECONDS); if (!config.isPluginMode()) {
scheduler.scheduleAtFixedRate(this::checkProcess, 0, interval, TimeUnit.SECONDS);
if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) {
int analysisInterval = config.getAi().getDeepseek().getAnalysisIntervalSeconds();
scheduler.scheduleAtFixedRate(this::runDeepSeekAnalysis, analysisInterval, analysisInterval, 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() { private void checkProcess() {
try { try {
AlertEvent event = processMonitor.checkAndRestart(); AlertEvent event = processMonitor.checkAndRestart();
if (event != null) { if (event != null) handleAlert(event);
logger.warn("Process alert: {}", event.getMessage());
handleAlert(event);
}
} catch (Exception e) { } catch (Exception e) {
logger.error("Process check failed: {}", e.getMessage()); logger.error("Process check failed: {}", e.getMessage());
} }
@@ -236,58 +264,30 @@ public class ServerGuardService implements CommandLineRunner {
private void collectAndAnalyzeMetrics() { private void collectAndAnalyzeMetrics() {
try { 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(); MetricsData data = resourceMonitor.collectMetrics();
deepSeekClient.addMetrics(data); deepSeekClient.addMetrics(data);
adaptiveThreshold.addMetric(data); adaptiveThreshold.addMetric(data);
anomalyDetector.addMetric(data); anomalyDetector.addMetric(data);
timeSeriesPredictor.addMetric(data); timeSeriesPredictor.addMetric(data);
List<AlertEvent> resourceAlerts = resourceMonitor.checkThresholds(data); List<AlertEvent> resourceAlerts = resourceMonitor.checkThresholds(data);
for (AlertEvent alert : resourceAlerts) { for (AlertEvent alert : resourceAlerts) handleAlert(alert);
handleAlert(alert);
}
AlertEvent connectionAlert = connectionMonitor.checkConnectionFlood();
if (connectionAlert != null) {
handleAlert(connectionAlert);
}
List<AlertEvent> logAlerts = logMonitor.analyzeLogs();
for (AlertEvent alert : logAlerts) {
handleAlert(alert);
}
if (config.getAi().isEnabled() && config.getAi().isUseIsolationForest()) { if (config.getAi().isEnabled() && config.getAi().isUseIsolationForest()) {
AlertEvent aiAlert = anomalyDetector.detectAnomaly(data); AlertEvent aiAlert = anomalyDetector.detectAnomaly(data);
if (aiAlert != null) { if (aiAlert != null) handleAlert(aiAlert);
handleAlert(aiAlert);
}
} }
if (config.getAi().isEnabled() && config.getAi().isUsePrediction()) { if (config.getAi().isEnabled() && config.getAi().isUsePrediction()) {
TimeSeriesPredictor.PredictionResult prediction = timeSeriesPredictor.predict(); TimeSeriesPredictor.PredictionResult prediction = timeSeriesPredictor.predict();
if (prediction != null && prediction.isWillBreachThreshold() && prediction.getConfidence() > 0.6) { if (prediction != null && prediction.isWillBreachThreshold() && prediction.getConfidence() > 0.6) {
logger.warn("TPS Prediction: {} (confidence: {:.0f}%) - {}", logger.warn("TPS Prediction: {} (confidence: {:.0f}%) — {}",
prediction.getPredictedValue(), prediction.getPredictedValue(), prediction.getConfidence() * 100, prediction.getRecommendation());
prediction.getConfidence() * 100,
prediction.getRecommendation());
} }
} }
List<AlertEvent> attackAlerts = attackDetector.analyzeThreats(); List<AlertEvent> attackAlerts = attackDetector.analyzeThreats();
for (AlertEvent alert : attackAlerts) { for (AlertEvent alert : attackAlerts) handleAlert(alert);
handleAlert(alert);
}
} catch (Exception e) { } catch (Exception e) {
logger.error("Metrics collection failed: {}", e.getMessage()); logger.error("Metrics collection failed: {}", e.getMessage());
@@ -295,19 +295,13 @@ public class ServerGuardService implements CommandLineRunner {
} }
private void runDeepSeekAnalysis() { private void runDeepSeekAnalysis() {
if (!deepSeekClient.isEnabled()) { if (!deepSeekClient.isEnabled()) return;
return;
}
try { try {
deepSeekClient.getServerHealthReportAsync().thenAccept(healthReport -> { deepSeekClient.getServerHealthReportAsync().thenAccept(healthReport -> {
if (healthReport == null) { if (healthReport == null) return;
return;
}
logger.info("=== DeepSeek AI Health Report ==="); logger.info("=== DeepSeek AI Health Report ===");
logger.info("Status: {}", healthReport.getSeverity()); logger.info("Status: {}", healthReport.getSeverity());
logger.info("Summary: {}", healthReport.getDescription()); logger.info("Summary: {}", healthReport.getDescription());
if ("critical".equals(healthReport.getSeverity()) || "warning".equals(healthReport.getSeverity())) { if ("critical".equals(healthReport.getSeverity()) || "warning".equals(healthReport.getSeverity())) {
for (String action : healthReport.getRecommendedActions()) { for (String action : healthReport.getRecommendedActions()) {
logger.warn("Recommendation: {}", action); logger.warn("Recommendation: {}", action);
@@ -319,56 +313,32 @@ public class ServerGuardService implements CommandLineRunner {
} }
} }
// ─── 告警处理管道 ──────────────────────────────────
private void handleAlert(AlertEvent alert) { private void handleAlert(AlertEvent alert) {
deepSeekClient.addAlert(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()) { if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) {
deepSeekClient.analyzeAlertAsync(alert).thenAccept(analysis -> { deepSeekClient.analyzeAlertAsync(alert).thenAccept(analysis -> {
if (analysis != null) { if (analysis != null) {
logger.info(" [DeepSeek AI Analysis]"); logger.info(" [DeepSeek AI Analysis] severity={}, rootCause={}", analysis.getSeverity(), analysis.getRootCause());
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);
}
}
alert.setRootCause(analysis.getRootCause()); alert.setRootCause(analysis.getRootCause());
alert.setSuggestedAction(String.join("; ", analysis.getRecommendedActions())); 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); 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() { public boolean isRunning() { return running.get(); }
return running.get(); public void stop() { running.set(false); }
}
public void stop() {
running.set(false);
}
} }