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