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