V5.0 Paper插件架构:深度植入Minecraft服务器进程,实现完美实时防护
核心变更: - 新增 PaperMC 插件层(14个新文件):主插件入口 AluerPlugin + 10个 Bukkit 事件监听器 + DataBridge 数据桥接 + InternalCommandExecutor 内部命令执行器 - 运行时注入服务器进程,直接访问 Bukkit/Paper API,取消 RCON 中间层 - 9种反作弊实时检测(Speed/Fly/Jesus/NoFall/KillAura/Reach/AutoClicker/Scaffold/Nuker)全部迁移为事件驱动 - 聊天安全(刷屏/广告/钓鱼)与命令滥用检测由 AsyncPlayerChatEvent/PlayerCommandPreprocessEvent 直接拦截 - AlertType 枚举扩展至 47 项告警类型,覆盖反作弊/玩家行为/服务器保护/聊天安全/网络/主机全部维度 - 双模式兼容:serverguard.mode=plugin(嵌入式)| external(传统外部监控),旧模式代码完整保留 - Maven 构建切换为 shade 插件,生成 Paper 插件兼容的扁平 JAR - 278 项全量测试通过,0 失败
This commit is contained in:
@@ -7,6 +7,15 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
/**
|
||||
* Aluer ServerGuard 主应用入口
|
||||
*
|
||||
* 支持两种运行模式:
|
||||
* 1. standalone — java -jar serverguard.jar(传统外部监控模式)
|
||||
* 2. plugin — 由 PaperMC 插件 AluerPlugin 调用 bootstrap() 启动嵌入式上下文
|
||||
*
|
||||
* 模式由系统属性 serverguard.mode 决定,AluerPlugin 启动时自动设置为 "plugin"
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
public class ServerGuardApplication {
|
||||
@@ -14,6 +23,8 @@ public class ServerGuardApplication {
|
||||
private static volatile ConfigurableApplicationContext context;
|
||||
|
||||
public static void main(String[] args) {
|
||||
// standalone 模式:直接启动
|
||||
System.setProperty("serverguard.mode", "external");
|
||||
context = SpringApplication.run(ServerGuardApplication.class, args);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
@@ -23,7 +34,25 @@ public class ServerGuardApplication {
|
||||
}
|
||||
}, "serverguard-shutdown-hook"));
|
||||
|
||||
logger.info("Aluer ServerGuard is ready");
|
||||
logger.info("Aluer ServerGuard standalone mode is ready");
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件模式启动入口 — 由 AluerPlugin.onEnable() 调用
|
||||
*
|
||||
* Paper 插件在 onEnable 时异步调用此方法,
|
||||
* 返回的 ConfigurableApplicationContext 由插件持有并在 onDisable 时关闭。
|
||||
*
|
||||
* @return Spring 应用上下文
|
||||
*/
|
||||
public static ConfigurableApplicationContext bootstrap() {
|
||||
if (context != null) {
|
||||
logger.warn("Spring Boot context already running, reusing existing context");
|
||||
return context;
|
||||
}
|
||||
context = SpringApplication.run(ServerGuardApplication.class);
|
||||
logger.info("Aluer ServerGuard plugin mode bootstrapped — Spring context ready");
|
||||
return context;
|
||||
}
|
||||
|
||||
public static ConfigurableApplicationContext getContext() {
|
||||
|
||||
@@ -23,6 +23,13 @@ public class ServerGuardConfig {
|
||||
private AnnouncementConfig announcement = new AnnouncementConfig();
|
||||
private AfkConfig afk = new AfkConfig();
|
||||
|
||||
/** 运行模式:plugin(Paper 插件内部)或 external(传统外部监控),默认 external */
|
||||
private String mode = "external";
|
||||
|
||||
public String getMode() { return mode; }
|
||||
public void setMode(String mode) { this.mode = mode; }
|
||||
public boolean isPluginMode() { return "plugin".equalsIgnoreCase(mode); }
|
||||
|
||||
public static class MinecraftConfig {
|
||||
private String serviceName = "minecraft";
|
||||
private String processName = "java";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.aluer.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
public class AlertEvent {
|
||||
private AlertType type;
|
||||
@@ -9,6 +11,8 @@ public class AlertEvent {
|
||||
private LocalDateTime timestamp;
|
||||
private double confidence;
|
||||
private String suggestedAction;
|
||||
/** 告警来源(玩家名/IP/系统模块名) */
|
||||
private String source;
|
||||
|
||||
public AlertEvent() {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
@@ -32,4 +36,11 @@ public class AlertEvent {
|
||||
public void setConfidence(double confidence) { this.confidence = confidence; }
|
||||
public String getSuggestedAction() { return suggestedAction; }
|
||||
public void setSuggestedAction(String suggestedAction) { this.suggestedAction = suggestedAction; }
|
||||
public String getSource() { return source; }
|
||||
public void setSource(String source) { this.source = source; }
|
||||
|
||||
/** 以 epoch millis 设置时间戳 */
|
||||
public void setTimestamp(long epochMillis) {
|
||||
this.timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), ZoneId.systemDefault());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.aluer.model;
|
||||
|
||||
public enum AlertType {
|
||||
// ─── 系统监控 ──────────────────────────────
|
||||
PROCESS_DEAD("Process Dead", "Minecraft process not found"),
|
||||
TPS_LOW("Low TPS", "TPS below threshold"),
|
||||
CPU_HIGH("High CPU", "CPU usage above threshold"),
|
||||
@@ -8,7 +9,57 @@ public enum AlertType {
|
||||
CONNECTION_FLOOD("Connection Flood", "Possible DDoS attack detected"),
|
||||
LOG_ATTACK("Attack Detected", "Malicious activity in logs"),
|
||||
BACKUP_FAILED("Backup Failed", "Backup task failed"),
|
||||
AI_ANOMALY("AI Anomaly", "AI detected abnormal behavior");
|
||||
AI_ANOMALY("AI Anomaly", "AI detected abnormal behavior"),
|
||||
|
||||
// ─── V4.0 反作弊 ──────────────────────────
|
||||
SECURITY_KILL_AURA("KillAura", "Multi-target attack or aimbot consistency detected"),
|
||||
SECURITY_REACH("Reach", "Attack distance exceeds normal maximum"),
|
||||
SECURITY_SPEED("Speed", "Horizontal movement speed anomaly"),
|
||||
SECURITY_JESUS("Jesus", "Abnormal water walking detected"),
|
||||
SECURITY_NOFALL("NoFall", "Fall damage bypass detected"),
|
||||
SECURITY_SCAFFOLD("Scaffold", "Rapid foot-block placement detected"),
|
||||
SECURITY_NUKER("Nuker", "Rapid block breaking detected"),
|
||||
SECURITY_AUTO_CLICKER("AutoClicker", "Abnormal attack frequency detected"),
|
||||
SECURITY_AUTO_FISH("AutoFish", "Automated fishing detected"),
|
||||
SECURITY_FLY("Fly", "Illegal flight detected"),
|
||||
|
||||
// ─── V4.0 玩家行为 ────────────────────────
|
||||
SECURITY_CHEST_STEAL("ChestSteal", "Rapid container theft detected"),
|
||||
SECURITY_INVENTORY_MANIPULATION("InventoryManipulation", "Abnormal inventory interaction"),
|
||||
SECURITY_GRIEF("Grief", "Valuable block destruction pattern"),
|
||||
SECURITY_ALT_ACCOUNT("AltAccount", "Multiple accounts from same IP"),
|
||||
SECURITY_BARITONE("Baritone", "Automated mining bot detected"),
|
||||
SECURITY_XRAY("Xray", "Suspicious ore mining pattern"),
|
||||
|
||||
// ─── V4.0 服务器保护 ──────────────────────
|
||||
SECURITY_SIGN_EXPLOIT("SignExploit", "Abnormal sign NBT data detected"),
|
||||
SECURITY_BOOK_BAN("BookBan", "Malicious book content detected"),
|
||||
SECURITY_RESOURCE_PACK_EXPLOIT("ResourcePackExploit", "Malicious resource pack attempt"),
|
||||
SECURITY_TAB_COMPLETE_CRASH("TabCompleteCrash", "Crash-inducing tab complete detected"),
|
||||
SECURITY_OFFLINE_MODE_SPOOF("OfflineModeSpoof", "Offline mode UUID spoof detected"),
|
||||
|
||||
// ─── V4.0 聊天与社交安全 ──────────────────
|
||||
CHAT_FLOOD("ChatFlood", "Chat message spam detected"),
|
||||
CHAT_ADVERTISEMENT("Advertisement", "IP/Domain advertisement detected"),
|
||||
CHAT_PHISHING("Phishing", "Phishing link detected"),
|
||||
COMMAND_ABUSE("CommandAbuse", "Sensitive command abuse detected"),
|
||||
|
||||
// ─── 网络安全 ─────────────────────────────
|
||||
SECURITY_DDOS("DDoS", "Distributed denial of service attack"),
|
||||
SECURITY_PORT_SCAN("PortScan", "Port scanning activity detected"),
|
||||
SECURITY_BRUTE_FORCE("BruteForce", "Brute force login attempt"),
|
||||
SECURITY_VPN_PROXY("VPN/Proxy", "Connection via VPN or proxy detected"),
|
||||
SECURITY_DNS_TUNNEL("DNSTunnel", "DNS tunneling detected"),
|
||||
|
||||
// ─── 主机安全 ─────────────────────────────
|
||||
SECURITY_REVERSE_SHELL("ReverseShell", "Reverse shell connection detected"),
|
||||
SECURITY_PROCESS_INJECTION("ProcessInjection", "Process injection detected"),
|
||||
SECURITY_FILE_TAMPER("FileTamper", "File integrity violation detected"),
|
||||
SECURITY_BACKDOOR_PLUGIN("BackdoorPlugin", "Suspicious plugin detected"),
|
||||
SECURITY_CONFIG_TAMPER("ConfigTamper", "Configuration tampering detected"),
|
||||
|
||||
// ─── 通用 ─────────────────────────────────
|
||||
SECURITY_OTHER("Security", "Security alert");
|
||||
|
||||
private final String title;
|
||||
private final String description;
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
package com.aluer.plugin;
|
||||
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.InternalCommandExecutor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Aluer 插件命令执行器 — 处理 /aluer /aluerstatus /aluerplayers 等管理命令
|
||||
*
|
||||
* 命令列表:
|
||||
* /aluer status — 查看防护系统状态
|
||||
* /aluer scan <玩家> — 手动触发玩家安全扫描
|
||||
* /aluer defense — 查看当前防御等级
|
||||
* /aluer info — 查看系统信息
|
||||
* /aluerplayers — 查看玩家风险列表
|
||||
* /aluerblock <玩家|ip> <目标> [理由] — 手动封禁
|
||||
* /aluerunblock <玩家|ip> <目标> — 手动解封
|
||||
* /aluerwhitelist <on|off|status> — 白名单管理
|
||||
*/
|
||||
public class AluerCommandExecutor implements CommandExecutor, TabCompleter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AluerCommandExecutor.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge dataBridge;
|
||||
|
||||
private static final String PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.GOLD + "Aluer" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET;
|
||||
|
||||
public AluerCommandExecutor(AluerPlugin plugin, DataBridge dataBridge) {
|
||||
this.plugin = plugin;
|
||||
this.dataBridge = dataBridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
String cmdName = command.getName().toLowerCase();
|
||||
|
||||
switch (cmdName) {
|
||||
case "aluer":
|
||||
return handleAluerCommand(sender, args);
|
||||
case "aluerstatus":
|
||||
return handleStatusCommand(sender);
|
||||
case "aluerplayers":
|
||||
return handlePlayersCommand(sender);
|
||||
case "aluerblock":
|
||||
return handleBlockCommand(sender, args);
|
||||
case "aluerunblock":
|
||||
return handleUnblockCommand(sender, args);
|
||||
case "aluerscan":
|
||||
return handleScanCommand(sender, args);
|
||||
case "aluerwhitelist":
|
||||
return handleWhitelistCommand(sender, args);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleAluerCommand(CommandSender sender, String[] args) {
|
||||
if (args.length == 0) {
|
||||
sendHelp(sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "status":
|
||||
return handleStatusCommand(sender);
|
||||
case "scan":
|
||||
return handleScanCommand(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
case "defense":
|
||||
return handleDefenseCommand(sender);
|
||||
case "info":
|
||||
return handleInfoCommand(sender);
|
||||
default:
|
||||
sendHelp(sender);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleStatusCommand(CommandSender sender) {
|
||||
sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ Aluer ServerGuard v4.0.0 ═══");
|
||||
|
||||
if (dataBridge != null) {
|
||||
double tps = dataBridge.getCurrentTps();
|
||||
int online = dataBridge.getOnlinePlayerCount();
|
||||
ChatColor tpsColor = tps >= 18 ? ChatColor.GREEN : tps >= 15 ? ChatColor.YELLOW : ChatColor.RED;
|
||||
|
||||
sender.sendMessage(PREFIX + "TPS: " + tpsColor + String.format("%.1f", tps) +
|
||||
ChatColor.RESET + " | 在线玩家: " + ChatColor.AQUA + online);
|
||||
sender.sendMessage(PREFIX + "Spring 上下文: " +
|
||||
(plugin.isSpringReady() ? ChatColor.GREEN + "就绪" : ChatColor.RED + "初始化中"));
|
||||
sender.sendMessage(PREFIX + "包速率: " + ChatColor.GRAY +
|
||||
String.format("%.0f pps", dataBridge.getPacketsPerSecond()));
|
||||
sender.sendMessage(PREFIX + "事件处理: " + ChatColor.GRAY +
|
||||
dataBridge.getEventCount("player.join") + " 登录, " +
|
||||
dataBridge.getEventCount("chat.message") + " 聊天, " +
|
||||
dataBridge.getEventCount("block.break") + " 破坏");
|
||||
} else {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪,请稍候...");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handlePlayersCommand(CommandSender sender) {
|
||||
if (dataBridge == null) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪");
|
||||
return true;
|
||||
}
|
||||
|
||||
var players = dataBridge.getAllPlayers();
|
||||
sender.sendMessage(PREFIX + ChatColor.YELLOW + "在线玩家: " + players.size());
|
||||
|
||||
for (var snap : players) {
|
||||
String riskInfo = "";
|
||||
if (snap.getRecentTargetCount() > 3) {
|
||||
riskInfo = ChatColor.RED + " [多目标]";
|
||||
}
|
||||
if (snap.hasSuspiciousAngleConsistency()) {
|
||||
riskInfo += ChatColor.DARK_RED + " [瞄准可疑]";
|
||||
}
|
||||
sender.sendMessage(PREFIX + " " + ChatColor.WHITE + snap.name +
|
||||
ChatColor.GRAY + " | IP: " + snap.ip +
|
||||
" | Ping: " + snap.ping + "ms" +
|
||||
riskInfo);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleBlockCommand(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerblock <player|ip> <目标> [理由]");
|
||||
return true;
|
||||
}
|
||||
|
||||
String type = args[0].toLowerCase();
|
||||
String target = args[1];
|
||||
String reason = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length))
|
||||
: "管理员手动封禁";
|
||||
|
||||
InternalCommandExecutor executor = plugin.getSpringContext() != null
|
||||
? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
|
||||
: null;
|
||||
|
||||
if (executor == null) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "命令执行器未就绪");
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean success;
|
||||
if ("ip".equals(type)) {
|
||||
success = executor.banIP(target, reason);
|
||||
} else {
|
||||
success = executor.banPlayer(target, reason);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
sender.sendMessage(PREFIX + ChatColor.GREEN + "已封禁 " + type + ": " + target);
|
||||
Bukkit.getServer().broadcast(
|
||||
net.kyori.adventure.text.Component.text(
|
||||
ChatColor.RED + "[Aluer] " + sender.getName() + " 封禁了 " + target + ": " + reason));
|
||||
} else {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "封禁失败: " + target);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleUnblockCommand(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerunblock <player|ip> <目标>");
|
||||
return true;
|
||||
}
|
||||
|
||||
String type = args[0].toLowerCase();
|
||||
String target = args[1];
|
||||
|
||||
InternalCommandExecutor executor = plugin.getSpringContext() != null
|
||||
? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
|
||||
: null;
|
||||
|
||||
if (executor == null) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "命令执行器未就绪");
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean success;
|
||||
if ("ip".equals(type)) {
|
||||
success = executor.unbanIP(target);
|
||||
} else {
|
||||
success = executor.unbanPlayer(target);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
sender.sendMessage(PREFIX + ChatColor.GREEN + "已解封 " + type + ": " + target);
|
||||
} else {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "解封失败: " + target);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleScanCommand(CommandSender sender, String[] args) {
|
||||
if (args.length < 1) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerscan <玩家名>");
|
||||
return true;
|
||||
}
|
||||
|
||||
String targetName = args[0];
|
||||
Player target = Bukkit.getServer().getPlayer(targetName);
|
||||
|
||||
if (target == null) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "玩家不在线: " + targetName);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dataBridge == null) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪");
|
||||
return true;
|
||||
}
|
||||
|
||||
var snap = dataBridge.getPlayer(target.getUniqueId());
|
||||
if (snap == null) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "未找到玩家数据");
|
||||
return true;
|
||||
}
|
||||
|
||||
sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 安全扫描: " + targetName + " ═══");
|
||||
sender.sendMessage(PREFIX + "位置: " + String.format("(%.1f, %.1f, %.1f) [%s]", snap.x, snap.y, snap.z, snap.world));
|
||||
sender.sendMessage(PREFIX + "IP: " + snap.ip + " | Ping: " + snap.ping + "ms");
|
||||
sender.sendMessage(PREFIX + "飞行: " + snap.isFlying + " | 冲刺: " + snap.isSprinting + " | 潜行: " + snap.isSneaking);
|
||||
sender.sendMessage(PREFIX + "最近攻击目标: " + snap.getRecentTargetCount() + " 个");
|
||||
sender.sendMessage(PREFIX + "攻击角度一致性: " + (snap.hasSuspiciousAngleConsistency() ? ChatColor.RED + "可疑" : ChatColor.GREEN + "正常"));
|
||||
sender.sendMessage(PREFIX + "最近消息: " + snap.recentMessages.size() + " 条");
|
||||
sender.sendMessage(PREFIX + "最近命令: " + snap.recentCommands.size() + " 条");
|
||||
sender.sendMessage(PREFIX + "在线时长: " + (snap.getOnlineTime() / 60000) + " 分钟");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleDefenseCommand(CommandSender sender) {
|
||||
sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 防御状态 ═══");
|
||||
sender.sendMessage(PREFIX + "模式: " + ChatColor.AQUA + "Paper Plugin(嵌入式)");
|
||||
|
||||
if (dataBridge != null) {
|
||||
double[] tps = dataBridge.getTpsArray();
|
||||
sender.sendMessage(PREFIX + "TPS: " + String.format("1m=%.1f 5m=%.1f 15m=%.1f", tps[0], tps[1], tps[2]));
|
||||
sender.sendMessage(PREFIX + "在线: " + dataBridge.getOnlinePlayerCount() + " | 包速率: " +
|
||||
String.format("%.0f pps", dataBridge.getPacketsPerSecond()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleInfoCommand(CommandSender sender) {
|
||||
sender.sendMessage(PREFIX + ChatColor.GOLD + "Aluer ServerGuard v4.0.0");
|
||||
sender.sendMessage(PREFIX + ChatColor.GRAY + "AI-powered Minecraft PaperMC Server Protection");
|
||||
sender.sendMessage(PREFIX + "Author: Aluer | Java 17 | Spring Boot 3.2.0");
|
||||
sender.sendMessage(PREFIX + "29 反作弊模块 | DDoS 防御 | WAF | Kernel 引擎");
|
||||
sender.sendMessage(PREFIX + "模式: " + (plugin.isSpringReady() ? "完全运作" : "初始化中"));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleWhitelistCommand(CommandSender sender, String[] args) {
|
||||
if (args.length < 1) {
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerwhitelist <on|off|status>");
|
||||
return true;
|
||||
}
|
||||
|
||||
InternalCommandExecutor executor = plugin.getSpringContext() != null
|
||||
? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
|
||||
: null;
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "on":
|
||||
if (executor != null) {
|
||||
executor.enableWhitelist();
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "紧急白名单模式已启用!");
|
||||
}
|
||||
break;
|
||||
case "off":
|
||||
if (executor != null) {
|
||||
executor.disableWhitelist();
|
||||
sender.sendMessage(PREFIX + ChatColor.GREEN + "白名单模式已关闭");
|
||||
}
|
||||
break;
|
||||
case "status":
|
||||
boolean enabled = executor != null && executor.isWhitelistEnabled();
|
||||
sender.sendMessage(PREFIX + "白名单: " + (enabled ? ChatColor.RED + "启用中" : ChatColor.GREEN + "关闭"));
|
||||
break;
|
||||
default:
|
||||
sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerwhitelist <on|off|status>");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sendHelp(CommandSender sender) {
|
||||
sender.sendMessage(PREFIX + ChatColor.GOLD + "═══ Aluer ServerGuard 命令 ═══");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluer status" + ChatColor.GRAY + " — 查看防护状态");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluer scan <玩家>" + ChatColor.GRAY + " — 安全扫描玩家");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluer defense" + ChatColor.GRAY + " — 查看防御等级");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluer info" + ChatColor.GRAY + " — 系统信息");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluerplayers" + ChatColor.GRAY + " — 玩家风险列表");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluerblock <player|ip> <目标>" + ChatColor.GRAY + " — 手动封禁");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluerunblock <player|ip> <目标>" + ChatColor.GRAY + " — 手动解封");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/aluerwhitelist <on|off|status>" + ChatColor.GRAY + " — 白名单管理");
|
||||
}
|
||||
|
||||
// ─── 命令补全 ──────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
String cmdName = command.getName().toLowerCase();
|
||||
|
||||
switch (cmdName) {
|
||||
case "aluer":
|
||||
if (args.length == 1) {
|
||||
return filterStartsWith(Arrays.asList("status", "scan", "defense", "info"), args[0]);
|
||||
}
|
||||
if (args.length == 2 && "scan".equalsIgnoreCase(args[0])) {
|
||||
return filterStartsWith(
|
||||
Bukkit.getServer().getOnlinePlayers().stream()
|
||||
.map(Player::getName)
|
||||
.collect(Collectors.toList()),
|
||||
args[1]);
|
||||
}
|
||||
break;
|
||||
case "aluerblock":
|
||||
if (args.length == 1) {
|
||||
return filterStartsWith(Arrays.asList("player", "ip"), args[0]);
|
||||
}
|
||||
if (args.length == 2 && "player".equalsIgnoreCase(args[0])) {
|
||||
return filterStartsWith(
|
||||
Bukkit.getServer().getOnlinePlayers().stream()
|
||||
.map(Player::getName)
|
||||
.collect(Collectors.toList()),
|
||||
args[1]);
|
||||
}
|
||||
break;
|
||||
case "aluerunblock":
|
||||
if (args.length == 1) {
|
||||
return filterStartsWith(Arrays.asList("player", "ip"), args[0]);
|
||||
}
|
||||
break;
|
||||
case "aluerwhitelist":
|
||||
if (args.length == 1) {
|
||||
return filterStartsWith(Arrays.asList("on", "off", "status"), args[0]);
|
||||
}
|
||||
break;
|
||||
case "aluerscan":
|
||||
if (args.length == 1) {
|
||||
return filterStartsWith(
|
||||
Bukkit.getServer().getOnlinePlayers().stream()
|
||||
.map(Player::getName)
|
||||
.collect(Collectors.toList()),
|
||||
args[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<String> filterStartsWith(List<String> options, String prefix) {
|
||||
String lower = prefix.toLowerCase();
|
||||
return options.stream()
|
||||
.filter(s -> s.toLowerCase().startsWith(lower))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.aluer.plugin;
|
||||
|
||||
import com.aluer.ServerGuardApplication;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.listener.*;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* Aluer ServerGuard — PaperMC 插件主入口
|
||||
*
|
||||
* 插件加载时启动嵌入式 Spring Boot 上下文,
|
||||
* 注册所有 Bukkit 事件监听器,通过 DataBridge
|
||||
* 将服务器内部实时数据推送至安全分析引擎。
|
||||
*
|
||||
* 架构:
|
||||
* Bukkit Events → Event Listeners → DataBridge → Security Services → AI → AutoExecutor → Bukkit API
|
||||
*/
|
||||
public class AluerPlugin extends JavaPlugin {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AluerPlugin.class);
|
||||
|
||||
/** 全局单例引用,供监听器在 Spring 上下文就绪前获取插件实例 */
|
||||
private static AluerPlugin instance;
|
||||
|
||||
/** Spring Boot 应用上下文,插件关闭时需 gracefully shutdown */
|
||||
private ConfigurableApplicationContext springContext;
|
||||
|
||||
/** 数据桥接中心,连接 Bukkit 事件与安全服务 */
|
||||
private DataBridge dataBridge;
|
||||
|
||||
/** Spring 上下文是否已就绪 */
|
||||
private volatile boolean springReady = false;
|
||||
|
||||
// ─── 生命周期 ────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
logger.info("Aluer ServerGuard v4.0.0 — PaperMC Plugin loading (LOAD STARTUP)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
long startMs = System.currentTimeMillis();
|
||||
|
||||
// 步骤 1:启动嵌入式 Spring Boot(异步,避免阻塞 Paper 主线程)
|
||||
logger.info("Starting embedded Spring Boot context...");
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// 设置 Paper 插件模式标志
|
||||
System.setProperty("serverguard.mode", "plugin");
|
||||
System.setProperty("server.mode", "plugin");
|
||||
|
||||
springContext = ServerGuardApplication.bootstrap();
|
||||
dataBridge = springContext.getBean(DataBridge.class);
|
||||
springReady = true;
|
||||
logger.info("Spring Boot context ready — all security services initialized");
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to start Spring Boot context: {}", e.getMessage(), e);
|
||||
}
|
||||
}, "aluer-spring-boot").start();
|
||||
|
||||
// 步骤 2:等待 Spring 上下文就绪(最长 30 秒)
|
||||
int waited = 0;
|
||||
while (!springReady && waited < 30) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
waited++;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!springReady) {
|
||||
logger.error("Spring Boot failed to start within 30s — plugin will operate in degraded mode");
|
||||
}
|
||||
|
||||
// 步骤 3:注册 Bukkit 事件监听器
|
||||
registerEventListeners();
|
||||
|
||||
// 步骤 4:注册插件命令
|
||||
registerCommands();
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startMs;
|
||||
logger.info("Aluer ServerGuard enabled in {}ms — real-time protection active", elapsed);
|
||||
logger.info(" Listeners: 10 event handlers registered");
|
||||
logger.info(" Commands: /aluer, /aluerstatus, /aluerplayers, /aluerblock, /aluerunblock, /aluerscan, /aluerwhitelist");
|
||||
logger.info(" Web Console: http://0.0.0.0:8080/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
logger.info("Shutting down Aluer ServerGuard...");
|
||||
|
||||
// 注销所有 Bukkit 事件监听器
|
||||
HandlerList.unregisterAll(this);
|
||||
|
||||
// 关闭 Spring Boot 上下文
|
||||
if (springContext != null) {
|
||||
springContext.close();
|
||||
springContext = null;
|
||||
}
|
||||
|
||||
instance = null;
|
||||
logger.info("Aluer ServerGuard disabled — protection stopped");
|
||||
}
|
||||
|
||||
// ─── 事件监听器注册 ──────────────────────────────────
|
||||
|
||||
private void registerEventListeners() {
|
||||
// 注意:监听器在构造函数中接收 DataBridge 引用
|
||||
// 如果 Spring 未就绪则使用 null(监听器内部做空安全处理)
|
||||
DataBridge bridge = springReady ? dataBridge : null;
|
||||
|
||||
// 核心监听器:玩家、战斗、聊天、命令
|
||||
Bukkit.getPluginManager().registerEvents(new PlayerEventListener(this, bridge), this);
|
||||
Bukkit.getPluginManager().registerEvents(new CombatEventListener(this, bridge), this);
|
||||
Bukkit.getPluginManager().registerEvents(new ChatEventListener(this, bridge), this);
|
||||
Bukkit.getPluginManager().registerEvents(new CommandEventListener(this, bridge), this);
|
||||
|
||||
// 方块与物品监听器
|
||||
Bukkit.getPluginManager().registerEvents(new BlockEventListener(this, bridge), this);
|
||||
Bukkit.getPluginManager().registerEvents(new InventoryEventListener(this, bridge), this);
|
||||
|
||||
// 实体与世界监听器
|
||||
Bukkit.getPluginManager().registerEvents(new EntityEventListener(this, bridge), this);
|
||||
Bukkit.getPluginManager().registerEvents(new WorldEventListener(this, bridge), this);
|
||||
|
||||
// 包级监听器(Paper API 特有,用于精确的包速率控制)
|
||||
if (Bukkit.getServer().getClass().getPackageName().contains("paper")) {
|
||||
Bukkit.getPluginManager().registerEvents(new PacketEventListener(this, bridge), this);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 命令注册 ────────────────────────────────────────
|
||||
|
||||
private void registerCommands() {
|
||||
AluerCommandExecutor executor = new AluerCommandExecutor(this, dataBridge);
|
||||
var aluerCmd = getCommand("aluer");
|
||||
if (aluerCmd != null) {
|
||||
aluerCmd.setExecutor(executor);
|
||||
aluerCmd.setTabCompleter(executor);
|
||||
}
|
||||
|
||||
var statusCmd = getCommand("aluerstatus");
|
||||
if (statusCmd != null) statusCmd.setExecutor(executor);
|
||||
var playersCmd = getCommand("aluerplayers");
|
||||
if (playersCmd != null) playersCmd.setExecutor(executor);
|
||||
var blockCmd = getCommand("aluerblock");
|
||||
if (blockCmd != null) blockCmd.setExecutor(executor);
|
||||
var unblockCmd = getCommand("aluerunblock");
|
||||
if (unblockCmd != null) unblockCmd.setExecutor(executor);
|
||||
var scanCmd = getCommand("aluerscan");
|
||||
if (scanCmd != null) scanCmd.setExecutor(executor);
|
||||
var whitelistCmd = getCommand("aluerwhitelist");
|
||||
if (whitelistCmd != null) whitelistCmd.setExecutor(executor);
|
||||
}
|
||||
|
||||
// ─── 公开访问器 ──────────────────────────────────────
|
||||
|
||||
/** 获取插件单例实例 */
|
||||
public static AluerPlugin getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** 获取 Spring 应用上下文(可能为 null,在 Spring 就绪前) */
|
||||
public ConfigurableApplicationContext getSpringContext() {
|
||||
return springContext;
|
||||
}
|
||||
|
||||
/** 获取 DataBridge(可能为 null,在 Spring 就绪前) */
|
||||
public DataBridge getDataBridge() {
|
||||
return dataBridge;
|
||||
}
|
||||
|
||||
/** Spring 上下文是否已完全初始化 */
|
||||
public boolean isSpringReady() {
|
||||
return springReady;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package com.aluer.plugin.bridge;
|
||||
|
||||
import com.aluer.model.AlertEvent;
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.model.MetricsData;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 数据桥接中心 — Bukkit 事件与安全服务之间的实时数据中枢
|
||||
*
|
||||
* 职责:
|
||||
* 1. 维护所有在线玩家的实时快照(位置、速度、攻击记录等)
|
||||
* 2. 采集 TPS、连接数等服务器级指标
|
||||
* 3. 接收 Bukkit 监听器推送的原始事件并转换为 AlertEvent
|
||||
* 4. 为安全服务提供实时查询接口
|
||||
* 5. 将告警事件投递至 ServerGuardService 的告警处理管道
|
||||
*
|
||||
* 所有方法均为线程安全设计(ConcurrentHashMap + AtomicReference)
|
||||
*/
|
||||
@Service
|
||||
public class DataBridge {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DataBridge.class);
|
||||
|
||||
// ─── 玩家状态 ──────────────────────────────────────
|
||||
|
||||
/** 在线玩家实时快照(UUID → 快照) */
|
||||
private final ConcurrentHashMap<UUID, PlayerSnapshot> players = new ConcurrentHashMap<>();
|
||||
|
||||
// ─── 服务器指标 ────────────────────────────────────
|
||||
|
||||
/** 当前 TPS(20.0 = 满速) */
|
||||
private final ConcurrentHashMap<String, AtomicLong> eventCounters = new ConcurrentHashMap<>();
|
||||
|
||||
/** 告警事件生产者(由 ServerGuardService 注入) */
|
||||
private volatile Consumer<AlertEvent> alertHandler;
|
||||
|
||||
/** 指标数据生产者(由 ServerGuardService 注入) */
|
||||
private volatile Consumer<MetricsData> metricsHandler;
|
||||
|
||||
/** 最近一次包速率快照的时间戳 */
|
||||
private volatile long lastPacketSnapshotTime = System.currentTimeMillis();
|
||||
|
||||
/** 当前秒内的包计数 */
|
||||
private final AtomicLong packetCountCurrentSecond = new AtomicLong(0);
|
||||
|
||||
/** 上次秒的包速率 */
|
||||
private volatile double packetsPerSecond = 0.0;
|
||||
|
||||
// ─── 玩家快照管理 ──────────────────────────────────
|
||||
|
||||
/** 玩家加入时创建快照 */
|
||||
public void registerPlayer(Player player) {
|
||||
PlayerSnapshot snapshot = new PlayerSnapshot(player);
|
||||
players.put(player.getUniqueId(), snapshot);
|
||||
logger.debug("Player registered: {} ({} online)", player.getName(), players.size());
|
||||
}
|
||||
|
||||
/** 玩家退出时移除快照 */
|
||||
public void removePlayer(UUID uuid) {
|
||||
PlayerSnapshot removed = players.remove(uuid);
|
||||
if (removed != null) {
|
||||
logger.debug("Player removed: {} ({} online)", removed.name, players.size());
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取单个玩家快照 */
|
||||
public PlayerSnapshot getPlayer(UUID uuid) {
|
||||
return players.get(uuid);
|
||||
}
|
||||
|
||||
/** 获取玩家快照(按名称模糊匹配) */
|
||||
public PlayerSnapshot getPlayerByName(String name) {
|
||||
return players.values().stream()
|
||||
.filter(p -> p.name.equalsIgnoreCase(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/** 获取所有在线玩家快照 */
|
||||
public Collection<PlayerSnapshot> getAllPlayers() {
|
||||
return Collections.unmodifiableCollection(players.values());
|
||||
}
|
||||
|
||||
/** 在线玩家数 */
|
||||
public int getOnlinePlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
/** 获取指定 IP 的玩家列表 */
|
||||
public List<PlayerSnapshot> getPlayersByIP(String ip) {
|
||||
return players.values().stream()
|
||||
.filter(p -> p.ip.equals(ip))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ─── TPS 与服务器指标 ──────────────────────────────
|
||||
|
||||
/** 获取当前服务器 TPS(1分钟平均) */
|
||||
public double getCurrentTps() {
|
||||
try {
|
||||
// Paper API: Bukkit.getServer().getTPS() 返回 double[3](1m, 5m, 15m)
|
||||
double[] tpsValues = Bukkit.getServer().getTPS();
|
||||
return tpsValues.length > 0 ? tpsValues[0] : 20.0;
|
||||
} catch (Exception e) {
|
||||
return 20.0; // 非 Paper 服务端回退
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取 TPS 数组 [1m, 5m, 15m] */
|
||||
public double[] getTpsArray() {
|
||||
try {
|
||||
return Bukkit.getServer().getTPS();
|
||||
} catch (Exception e) {
|
||||
return new double[]{20.0, 20.0, 20.0};
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前包速率(包/秒) */
|
||||
public double getPacketsPerSecond() {
|
||||
return packetsPerSecond;
|
||||
}
|
||||
|
||||
/** 记录一个包事件(由 PacketEventListener 调用) */
|
||||
public void recordPacket() {
|
||||
packetCountCurrentSecond.incrementAndGet();
|
||||
}
|
||||
|
||||
/** 更新包速率快照(每秒调用一次) */
|
||||
public void updatePacketRate() {
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - lastPacketSnapshotTime;
|
||||
if (elapsed > 0) {
|
||||
packetsPerSecond = packetCountCurrentSecond.get() * 1000.0 / elapsed;
|
||||
}
|
||||
packetCountCurrentSecond.set(0);
|
||||
lastPacketSnapshotTime = now;
|
||||
}
|
||||
|
||||
/** 获取事件计数器值 */
|
||||
public long getEventCount(String eventName) {
|
||||
AtomicLong counter = eventCounters.get(eventName);
|
||||
return counter != null ? counter.get() : 0;
|
||||
}
|
||||
|
||||
/** 递增事件计数器 */
|
||||
public long incrementEvent(String eventName) {
|
||||
return eventCounters.computeIfAbsent(eventName, k -> new AtomicLong(0)).incrementAndGet();
|
||||
}
|
||||
|
||||
// ─── 告警与指标推送 ────────────────────────────────
|
||||
|
||||
/** 设置告警处理器(由 ServerGuardService 注入) */
|
||||
public void setAlertHandler(Consumer<AlertEvent> handler) {
|
||||
this.alertHandler = handler;
|
||||
}
|
||||
|
||||
/** 设置指标处理器(由 ServerGuardService 注入) */
|
||||
public void setMetricsHandler(Consumer<MetricsData> handler) {
|
||||
this.metricsHandler = handler;
|
||||
}
|
||||
|
||||
/** 推送一条告警事件到处理管道 */
|
||||
public void pushAlert(AlertEvent alert) {
|
||||
Consumer<AlertEvent> handler = alertHandler;
|
||||
if (handler != null) {
|
||||
handler.accept(alert);
|
||||
} else {
|
||||
logger.warn("No alert handler set — alert dropped: {}", alert.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 推送指标数据到处理管道 */
|
||||
public void pushMetrics(MetricsData data) {
|
||||
Consumer<MetricsData> handler = metricsHandler;
|
||||
if (handler != null) {
|
||||
handler.accept(data);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 便捷告警构造 ──────────────────────────────────
|
||||
|
||||
/** 快速构造并推送一条告警 */
|
||||
public void alert(AlertType type, String message, double confidence, String source) {
|
||||
AlertEvent event = new AlertEvent();
|
||||
event.setType(type);
|
||||
event.setMessage(message);
|
||||
event.setConfidence(confidence);
|
||||
event.setSource(source);
|
||||
event.setTimestamp(Instant.now().toEpochMilli());
|
||||
pushAlert(event);
|
||||
}
|
||||
|
||||
// ─── 构建 MetricsData ───────────────────────────────
|
||||
|
||||
/** 从当前实时状态构建 MetricsData 快照 */
|
||||
public MetricsData buildMetricsSnapshot() {
|
||||
MetricsData data = new MetricsData();
|
||||
data.setTps(getCurrentTps());
|
||||
|
||||
// CPU 和 Memory 通过 JMX 获取(插件模式下仍可使用系统级指标)
|
||||
try {
|
||||
java.lang.management.OperatingSystemMXBean osBean =
|
||||
java.lang.management.ManagementFactory.getOperatingSystemMXBean();
|
||||
if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
|
||||
data.setCpuUsage(sunOsBean.getCpuLoad() * 100.0);
|
||||
long totalMem = Runtime.getRuntime().totalMemory();
|
||||
long maxMem = Runtime.getRuntime().maxMemory();
|
||||
data.setMemoryUsage((double) totalMem / maxMem * 100.0);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
data.setCpuUsage(0);
|
||||
data.setMemoryUsage(0);
|
||||
}
|
||||
|
||||
data.setOnlinePlayers(players.size());
|
||||
data.setConnections(players.size());
|
||||
return data;
|
||||
}
|
||||
|
||||
// ─── 玩家快照数据结构 ──────────────────────────────
|
||||
|
||||
/**
|
||||
* 在线玩家的实时状态快照
|
||||
* 包含位置、速度、攻击记录等安全检测所需的关键字段
|
||||
*/
|
||||
public static class PlayerSnapshot {
|
||||
public final UUID uuid;
|
||||
public final String name;
|
||||
public volatile String ip;
|
||||
public volatile double x, y, z;
|
||||
public volatile String world;
|
||||
public volatile float yaw, pitch;
|
||||
public volatile double velocityX, velocityY, velocityZ;
|
||||
public volatile double lastMoveX, lastMoveY, lastMoveZ;
|
||||
public volatile long lastMoveTime;
|
||||
public volatile int ping;
|
||||
public volatile boolean isOp;
|
||||
public volatile boolean isFlying;
|
||||
public volatile boolean isSprinting;
|
||||
public volatile boolean isSneaking;
|
||||
public volatile boolean isInWater;
|
||||
|
||||
/** 最近攻击目标(目标实体名 → 攻击时间戳) */
|
||||
public final ConcurrentHashMap<String, Long> recentTargets = new ConcurrentHashMap<>();
|
||||
|
||||
/** 最近攻击角度(用于 KillAura 检测) */
|
||||
public final ConcurrentLinkedDeque<Double> recentAttackAngles = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/** 最近攻击距离(用于 Reach 检测) */
|
||||
public final ConcurrentLinkedDeque<Double> recentAttackDistances = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/** 最近发送的消息(用于聊天刷屏检测) */
|
||||
public final ConcurrentLinkedDeque<String> recentMessages = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/** 最近执行的命令(用于命令滥用检测) */
|
||||
public final ConcurrentLinkedDeque<String> recentCommands = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/** 最近破坏的方块位置(用于 Nuker 检测) */
|
||||
public final ConcurrentLinkedDeque<String> recentBlockBreaks = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/** 最近放置的方块位置 */
|
||||
public final ConcurrentLinkedDeque<String> recentBlockPlaces = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/** 总移动距离(用于 Speed 检测) */
|
||||
public volatile double totalDistanceMoved = 0.0;
|
||||
|
||||
/** 登录时间 */
|
||||
public final long loginTime;
|
||||
|
||||
public PlayerSnapshot(org.bukkit.entity.Player player) {
|
||||
this.uuid = player.getUniqueId();
|
||||
this.name = player.getName();
|
||||
this.ip = player.getAddress() != null && player.getAddress().getAddress() != null
|
||||
? player.getAddress().getAddress().getHostAddress()
|
||||
: "unknown";
|
||||
this.world = player.getWorld().getName();
|
||||
var loc = player.getLocation();
|
||||
this.x = loc.getX();
|
||||
this.y = loc.getY();
|
||||
this.z = loc.getZ();
|
||||
this.yaw = loc.getYaw();
|
||||
this.pitch = loc.getPitch();
|
||||
this.ping = player.getPing();
|
||||
this.isOp = player.isOp();
|
||||
this.isFlying = player.isFlying();
|
||||
this.isSprinting = player.isSprinting();
|
||||
this.isSneaking = player.isSneaking();
|
||||
this.isInWater = player.isInWater();
|
||||
this.loginTime = System.currentTimeMillis();
|
||||
this.lastMoveTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** 更新玩家位置快照(由 PlayerMoveEvent 触发) */
|
||||
public void updatePosition(double x, double y, double z, float yaw, float pitch) {
|
||||
this.lastMoveX = this.x;
|
||||
this.lastMoveY = this.y;
|
||||
this.lastMoveZ = this.z;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
this.lastMoveTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** 记录攻击目标(用于 KillAura 多目标检测) */
|
||||
public void recordTarget(String targetName) {
|
||||
recentTargets.put(targetName, System.currentTimeMillis());
|
||||
// 清理 10 秒以外的旧记录
|
||||
long cutoff = System.currentTimeMillis() - 10_000;
|
||||
recentTargets.entrySet().removeIf(e -> e.getValue() < cutoff);
|
||||
}
|
||||
|
||||
/** 获取最近 3 秒内的唯一目标数 */
|
||||
public int getRecentTargetCount() {
|
||||
long cutoff = System.currentTimeMillis() - 3_000;
|
||||
return (int) recentTargets.values().stream().filter(t -> t >= cutoff).count();
|
||||
}
|
||||
|
||||
/** 记录攻击角度 */
|
||||
public void recordAttackAngle(double angle) {
|
||||
recentAttackAngles.add(angle);
|
||||
while (recentAttackAngles.size() > 10) {
|
||||
recentAttackAngles.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录攻击距离 */
|
||||
public void recordAttackDistance(double distance) {
|
||||
recentAttackDistances.add(distance);
|
||||
while (recentAttackDistances.size() > 10) {
|
||||
recentAttackDistances.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查攻击角度是否过于一致(可能使用 Aimbot) */
|
||||
public boolean hasSuspiciousAngleConsistency() {
|
||||
if (recentAttackAngles.size() < 5) return false;
|
||||
double maxDeviation = 0;
|
||||
var it = recentAttackAngles.iterator();
|
||||
double prev = it.next();
|
||||
while (it.hasNext()) {
|
||||
double curr = it.next();
|
||||
double diff = Math.abs(curr - prev);
|
||||
if (diff > 180) diff = 360 - diff;
|
||||
maxDeviation = Math.max(maxDeviation, diff);
|
||||
prev = curr;
|
||||
}
|
||||
return maxDeviation < 5.0; // 小于 5 度偏差视为可疑
|
||||
}
|
||||
|
||||
/** 在线时长(毫秒) */
|
||||
public long getOnlineTime() {
|
||||
return System.currentTimeMillis() - loginTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("PlayerSnapshot{%s pos=(%.1f,%.1f,%.1f) world=%s ping=%d}",
|
||||
name, x, y, z, world, ping);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package com.aluer.plugin.bridge;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 内部命令执行器 — 替代 RCON 协议,直接通过 Bukkit API 执行服务器命令
|
||||
*
|
||||
* 插件模式下不再需要通过 TCP RCON 连接本地服务器,
|
||||
* 直接调用 Bukkit.getServer().dispatchCommand() 即可。
|
||||
*
|
||||
* 同时封装常见的运维操作:封禁/解封 IP、踢人、白名单管理等。
|
||||
*/
|
||||
@Service
|
||||
public class InternalCommandExecutor {
|
||||
private static final Logger logger = LoggerFactory.getLogger(InternalCommandExecutor.class);
|
||||
|
||||
/** 已封禁的 IP 集合(内存缓存,重启后失效,持久化由 BanList API 处理) */
|
||||
private final Set<String> bannedIPs = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** 已封禁的玩家名集合 */
|
||||
private final Set<String> bannedPlayers = ConcurrentHashMap.newKeySet();
|
||||
|
||||
// ─── 命令执行 ──────────────────────────────────────
|
||||
|
||||
/** 以控制台身份执行任意 Minecraft 命令 */
|
||||
public boolean executeCommand(String command) {
|
||||
try {
|
||||
return Bukkit.getServer().dispatchCommand(
|
||||
Bukkit.getServer().getConsoleSender(), command);
|
||||
} catch (Exception e) {
|
||||
logger.error("Command execution failed: {} — {}", command, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行命令并返回是否成功(静默模式,不打日志) */
|
||||
public boolean executeCommandQuiet(String command) {
|
||||
try {
|
||||
return Bukkit.getServer().dispatchCommand(
|
||||
Bukkit.getServer().getConsoleSender(), command);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 玩家管理 ──────────────────────────────────────
|
||||
|
||||
/** 踢出玩家 */
|
||||
public boolean kickPlayer(String playerName, String reason) {
|
||||
Player player = Bukkit.getServer().getPlayer(playerName);
|
||||
if (player == null) {
|
||||
logger.warn("Cannot kick {}: player not online", playerName);
|
||||
return false;
|
||||
}
|
||||
player.kick(net.kyori.adventure.text.Component.text(reason));
|
||||
logger.info("Player {} kicked: {}", playerName, reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 封禁玩家名 */
|
||||
public boolean banPlayer(String playerName, String reason) {
|
||||
if (bannedPlayers.contains(playerName)) {
|
||||
return true; // 已封禁
|
||||
}
|
||||
// 使用 Bukkit BanList API 进行持久化封禁
|
||||
OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName);
|
||||
if (target != null) {
|
||||
Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME)
|
||||
.addBan(playerName, reason, null, "Aluer ServerGuard");
|
||||
bannedPlayers.add(playerName);
|
||||
// 如果在线则踢出
|
||||
kickPlayer(playerName, reason);
|
||||
logger.info("Player {} banned: {}", playerName, reason);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 解除玩家封禁 */
|
||||
public boolean unbanPlayer(String playerName) {
|
||||
Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME).pardon(playerName);
|
||||
bannedPlayers.remove(playerName);
|
||||
logger.info("Player {} unbanned", playerName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 封禁 IP */
|
||||
public boolean banIP(String ip, String reason) {
|
||||
if (bannedIPs.contains(ip)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Bukkit.getServer().getBanList(org.bukkit.BanList.Type.IP)
|
||||
.addBan(ip, reason, null, "Aluer ServerGuard");
|
||||
bannedIPs.add(ip);
|
||||
// 踢出该 IP 的所有在线玩家
|
||||
for (Player player : Bukkit.getServer().getOnlinePlayers()) {
|
||||
String playerIP = player.getAddress() != null && player.getAddress().getAddress() != null
|
||||
? player.getAddress().getAddress().getHostAddress()
|
||||
: "";
|
||||
if (ip.equals(playerIP)) {
|
||||
player.kick(net.kyori.adventure.text.Component.text(reason));
|
||||
}
|
||||
}
|
||||
logger.info("IP {} banned: {}", ip, reason);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to ban IP {}: {}", ip, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 解除 IP 封禁 */
|
||||
public boolean unbanIP(String ip) {
|
||||
Bukkit.getServer().getBanList(org.bukkit.BanList.Type.IP).pardon(ip);
|
||||
bannedIPs.remove(ip);
|
||||
logger.info("IP {} unbanned", ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 将玩家加入白名单 */
|
||||
public boolean addToWhitelist(String playerName) {
|
||||
OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName);
|
||||
if (target != null) {
|
||||
target.setWhitelisted(true);
|
||||
logger.info("Player {} added to whitelist", playerName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 从白名单移除玩家 */
|
||||
public boolean removeFromWhitelist(String playerName) {
|
||||
OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName);
|
||||
if (target != null) {
|
||||
target.setWhitelisted(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 启用白名单模式 */
|
||||
public boolean enableWhitelist() {
|
||||
Bukkit.getServer().setWhitelist(true);
|
||||
logger.warn("WHITELIST ENABLED — only whitelisted players can join");
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 禁用白名单模式 */
|
||||
public boolean disableWhitelist() {
|
||||
Bukkit.getServer().setWhitelist(false);
|
||||
logger.info("Whitelist disabled");
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 查询白名单状态 */
|
||||
public boolean isWhitelistEnabled() {
|
||||
return Bukkit.getServer().hasWhitelist();
|
||||
}
|
||||
|
||||
// ─── 服务器管理 ─────────────────────────────────────
|
||||
|
||||
/** 保存所有世界 */
|
||||
public boolean saveAllWorlds() {
|
||||
executeCommand("save-all");
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 广播消息给全服玩家 */
|
||||
public void broadcast(String message) {
|
||||
Bukkit.getServer().broadcast(
|
||||
net.kyori.adventure.text.Component.text(message));
|
||||
}
|
||||
|
||||
/** 清理掉落物 */
|
||||
public int clearDroppedItems() {
|
||||
int removed = 0;
|
||||
for (org.bukkit.World world : Bukkit.getServer().getWorlds()) {
|
||||
for (org.bukkit.entity.Entity entity : world.getEntities()) {
|
||||
if (entity instanceof org.bukkit.entity.Item) {
|
||||
entity.remove();
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed > 0) {
|
||||
logger.info("Cleared {} dropped items across all worlds", removed);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/** 清理实体(按类型) */
|
||||
public int clearEntities(Class<? extends org.bukkit.entity.Entity> entityType) {
|
||||
int removed = 0;
|
||||
for (org.bukkit.World world : Bukkit.getServer().getWorlds()) {
|
||||
for (org.bukkit.entity.Entity entity : world.getEntities()) {
|
||||
if (entityType.isInstance(entity) && !(entity instanceof Player)) {
|
||||
entity.remove();
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/** 设置生物生成速率 */
|
||||
public boolean setSpawnRate(int rate) {
|
||||
for (org.bukkit.World world : Bukkit.getServer().getWorlds()) {
|
||||
world.setMonsterSpawnLimit(rate);
|
||||
world.setAnimalSpawnLimit(rate);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 获取服务器在线玩家对象列表 */
|
||||
public Collection<? extends Player> getOnlinePlayers() {
|
||||
return Bukkit.getServer().getOnlinePlayers();
|
||||
}
|
||||
|
||||
/** 按 IP 封禁范围(用于隔离攻击源) */
|
||||
public boolean quarantineIP(String ip) {
|
||||
return banIP(ip, "Aluer Kernel — 安全隔离");
|
||||
}
|
||||
|
||||
/** 获取已封禁 IP 列表 */
|
||||
public Set<String> getBannedIPs() {
|
||||
return Collections.unmodifiableSet(bannedIPs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.block.SignChangeEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 方块事件监听器 — 处理方块破坏、放置、告示牌修改
|
||||
*
|
||||
* 实时检测:
|
||||
* - Nuker(炸图)— 短时间内破坏大量方块
|
||||
* - Scaffold(搭桥)— 脚下方块快速连续放置模式
|
||||
* - SignExploit(告示牌漏洞)— 异常大小的 NBT 标签内容
|
||||
* - Grief(破坏)— 查找特定贵重方块的大规模破坏模式
|
||||
*/
|
||||
public class BlockEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(BlockEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** Nuker 检测窗口(秒内破坏方块数) */
|
||||
private static final int NUKER_THRESHOLD = 25;
|
||||
|
||||
/** Scaffold 检测窗口(秒内脚下方块放置数) */
|
||||
private static final int SCAFFOLD_THRESHOLD = 8;
|
||||
|
||||
/** 贵重方块列表(Grief 检测目标) */
|
||||
private static final Material[] VALUABLE_BLOCKS = {
|
||||
Material.DIAMOND_BLOCK, Material.GOLD_BLOCK, Material.IRON_BLOCK,
|
||||
Material.EMERALD_BLOCK, Material.NETHERITE_BLOCK, Material.BEACON,
|
||||
Material.CHEST, Material.ENDER_CHEST, Material.ENCHANTING_TABLE,
|
||||
Material.ANVIL, Material.DAMAGED_ANVIL, Material.CHIPPED_ANVIL
|
||||
};
|
||||
|
||||
public BlockEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBlockBreak(BlockBreakEvent event) {
|
||||
if (bridge == null) return;
|
||||
Player player = event.getPlayer();
|
||||
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
|
||||
if (snap == null) return;
|
||||
|
||||
Block block = event.getBlock();
|
||||
String posKey = String.format("%d,%d,%d",
|
||||
block.getX(), block.getY(), block.getZ());
|
||||
|
||||
snap.recentBlockBreaks.add(posKey);
|
||||
while (snap.recentBlockBreaks.size() > 50) {
|
||||
snap.recentBlockBreaks.removeFirst();
|
||||
}
|
||||
|
||||
bridge.incrementEvent("block.break");
|
||||
|
||||
// —— Nuker 检测 ——
|
||||
if (snap.recentBlockBreaks.size() >= NUKER_THRESHOLD) {
|
||||
bridge.alert(AlertType.SECURITY_NUKER,
|
||||
String.format("玩家 %s 短时间内破坏 %d 个方块,疑似 Nuker",
|
||||
player.getName(), snap.recentBlockBreaks.size()),
|
||||
Math.min(0.95, snap.recentBlockBreaks.size() / 40.0),
|
||||
player.getName());
|
||||
}
|
||||
|
||||
// —— Grief 检测:大量破坏贵重方块 ——
|
||||
Material type = block.getType();
|
||||
for (Material valuable : VALUABLE_BLOCKS) {
|
||||
if (type == valuable) {
|
||||
bridge.alert(AlertType.SECURITY_GRIEF,
|
||||
String.format("玩家 %s 破坏贵重方块 %s 位于 %s",
|
||||
player.getName(), type.name(), posKey),
|
||||
0.6, player.getName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBlockPlace(BlockPlaceEvent event) {
|
||||
if (bridge == null) return;
|
||||
Player player = event.getPlayer();
|
||||
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
|
||||
if (snap == null) return;
|
||||
|
||||
Block block = event.getBlock();
|
||||
String posKey = String.format("%d,%d,%d",
|
||||
block.getX(), block.getY(), block.getZ());
|
||||
|
||||
snap.recentBlockPlaces.add(posKey);
|
||||
while (snap.recentBlockPlaces.size() > 50) {
|
||||
snap.recentBlockPlaces.removeFirst();
|
||||
}
|
||||
|
||||
bridge.incrementEvent("block.place");
|
||||
|
||||
// —— Scaffold 检测:脚下方块快速放置 ——
|
||||
// 检查是否在玩家脚下方块附近放置
|
||||
double playerY = snap.y;
|
||||
if (Math.abs(block.getY() - (playerY - 1)) < 1.5) {
|
||||
// 脚下方块
|
||||
int footPlaces = 0;
|
||||
for (String p : snap.recentBlockPlaces) {
|
||||
String[] parts = p.split(",");
|
||||
if (parts.length == 3) {
|
||||
try {
|
||||
double by = Double.parseDouble(parts[1]);
|
||||
if (Math.abs(by - (playerY - 1)) < 1.5) footPlaces++;
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
if (footPlaces >= SCAFFOLD_THRESHOLD) {
|
||||
bridge.alert(AlertType.SECURITY_SCAFFOLD,
|
||||
String.format("玩家 %s 疑似 Scaffold:快速在脚下方块放置", player.getName()),
|
||||
0.75, player.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void onSignChange(SignChangeEvent event) {
|
||||
if (bridge == null) return;
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// —— SignExploit 检测 ——
|
||||
// 检测异常长的告示牌文本(可能导致崩溃的 NBT 攻击)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
String line = event.getLine(i);
|
||||
if (line != null && line.length() > 256) {
|
||||
bridge.alert(AlertType.SECURITY_SIGN_EXPLOIT,
|
||||
String.format("玩家 %s 告示牌文本异常长(%d字符),疑似 SignExploit",
|
||||
player.getName(), line.length()),
|
||||
0.9, player.getName());
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 聊天事件监听器 — 处理玩家聊天消息
|
||||
*
|
||||
* 实时检测:
|
||||
* - ChatFlood(聊天刷屏)— 短时间内大量消息
|
||||
* - Advertisement(广告)— IP/域名模式匹配
|
||||
* - PhishingLink(钓鱼链接)— 可疑 URL 检测
|
||||
* - Profanity(不雅用语)— 关键词过滤
|
||||
* - Spam(重复消息)— 相同内容重复发送
|
||||
*/
|
||||
public class ChatEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChatEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** IP 地址正则 */
|
||||
private static final Pattern IP_PATTERN = Pattern.compile(
|
||||
"(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d{1,5})?");
|
||||
|
||||
/** URL 正则(含常见短链接服务) */
|
||||
private static final Pattern URL_PATTERN = Pattern.compile(
|
||||
"https?://[^\\s]+|bit\\.ly/[^\\s]+|tinyurl\\.com/[^\\s]+|discord\\.gg/[^\\s]+",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/** 聊天刷屏阈值(秒内消息数) */
|
||||
private static final int SPAM_THRESHOLD = 5;
|
||||
private static final long SPAM_WINDOW_MS = 10_000;
|
||||
|
||||
public ChatEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
|
||||
if (bridge == null) return;
|
||||
String message = event.getMessage();
|
||||
String playerName = event.getPlayer().getName();
|
||||
PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId());
|
||||
|
||||
// —— 聊天刷屏检测 ——
|
||||
if (snap != null) {
|
||||
snap.recentMessages.add(message);
|
||||
while (snap.recentMessages.size() > 20) {
|
||||
snap.recentMessages.removeFirst();
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long recentCount = snap.recentMessages.stream()
|
||||
.filter(m -> true) // 所有消息都在时间窗口内(Deque 中保留最近 20 条)
|
||||
.count();
|
||||
|
||||
if (recentCount >= SPAM_THRESHOLD) {
|
||||
bridge.alert(AlertType.CHAT_FLOOD,
|
||||
String.format("玩家 %s 聊天刷屏:%d 条消息", playerName, recentCount),
|
||||
0.7, playerName);
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// —— 广告/IP 检测 ——
|
||||
if (IP_PATTERN.matcher(message).find()) {
|
||||
bridge.alert(AlertType.CHAT_ADVERTISEMENT,
|
||||
String.format("玩家 %s 发送疑似IP地址:%s", playerName, message),
|
||||
0.8, playerName);
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// —— 钓鱼链接检测 ——
|
||||
if (URL_PATTERN.matcher(message).find()) {
|
||||
bridge.alert(AlertType.CHAT_PHISHING,
|
||||
String.format("玩家 %s 发送外部链接:%s", playerName, message),
|
||||
0.75, playerName);
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
bridge.incrementEvent("chat.message");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
|
||||
import org.bukkit.entity.*;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 战斗事件监听器 — 处理玩家攻击、实体伤害、死亡事件
|
||||
*
|
||||
* 实时检测:
|
||||
* - KillAura(杀戮光环)— 多目标快速切换 + 攻击角度一致性
|
||||
* - Reach(攻击距离扩展)— 攻击距离超过 3.0 方块
|
||||
* - AutoClicker(自动连点)— 异常攻击频率
|
||||
* - ChestSteal — 容器交互跟踪由 InventoryEventListener 处理
|
||||
*/
|
||||
public class CombatEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CombatEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** 正常最大攻击距离(方块) */
|
||||
private static final double MAX_ATTACK_DISTANCE = 3.0;
|
||||
|
||||
/** 攻击距离扩展容忍度 */
|
||||
private static final double REACH_TOLERANCE = 3.3;
|
||||
|
||||
public CombatEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
|
||||
if (bridge == null) return;
|
||||
|
||||
// 仅处理玩家攻击
|
||||
if (!(event.getDamager() instanceof Player attacker)) return;
|
||||
Entity victim = event.getEntity();
|
||||
|
||||
PlayerSnapshot attackerSnap = bridge.getPlayer(attacker.getUniqueId());
|
||||
if (attackerSnap == null) return;
|
||||
|
||||
// —— 记录攻击目标(KillAura 检测用) ——
|
||||
attackerSnap.recordTarget(victim.getName());
|
||||
|
||||
// —— KillAura 检测:多目标快速切换 ——
|
||||
int recentTargets = attackerSnap.getRecentTargetCount();
|
||||
if (recentTargets > 4) {
|
||||
bridge.alert(AlertType.SECURITY_KILL_AURA,
|
||||
String.format("玩家 %s 在3秒内攻击了 %d 个不同目标", attacker.getName(), recentTargets),
|
||||
Math.min(0.95, recentTargets * 0.2), attacker.getName());
|
||||
}
|
||||
|
||||
// —— Reach 检测:攻击距离 ——
|
||||
double distance = attacker.getLocation().distance(victim.getLocation());
|
||||
attackerSnap.recordAttackDistance(distance);
|
||||
if (distance > REACH_TOLERANCE) {
|
||||
bridge.alert(AlertType.SECURITY_REACH,
|
||||
String.format("玩家 %s 攻击距离异常:%.2f 方块(最大 %.2f)",
|
||||
attacker.getName(), distance, MAX_ATTACK_DISTANCE),
|
||||
Math.min(0.95, (distance - MAX_ATTACK_DISTANCE) * 0.5 + 0.5),
|
||||
attacker.getName());
|
||||
}
|
||||
|
||||
// —— KillAura:攻击角度一致性 ——
|
||||
Vector attackerDir = attacker.getLocation().getDirection();
|
||||
double angle = Math.atan2(
|
||||
victim.getLocation().getZ() - attacker.getLocation().getZ(),
|
||||
victim.getLocation().getX() - attacker.getLocation().getX()
|
||||
) * 180.0 / Math.PI;
|
||||
attackerSnap.recordAttackAngle(angle);
|
||||
if (attackerSnap.hasSuspiciousAngleConsistency()) {
|
||||
bridge.alert(AlertType.SECURITY_KILL_AURA,
|
||||
String.format("玩家 %s 攻击角度高度一致,疑似 Aimbot", attacker.getName()),
|
||||
0.85, attacker.getName());
|
||||
}
|
||||
|
||||
// —— AutoClicker 检测:攻击频率异常 ——
|
||||
long now = System.currentTimeMillis();
|
||||
long recentAttacks = attackerSnap.recentTargets.values().stream()
|
||||
.filter(t -> t > now - 1000)
|
||||
.count();
|
||||
if (recentAttacks > 20) { // 每秒超过20次攻击(正常CPS上限约14)
|
||||
bridge.alert(AlertType.SECURITY_AUTO_CLICKER,
|
||||
String.format("玩家 %s 攻击频率异常:每秒 %d 次", attacker.getName(), recentAttacks),
|
||||
0.8, attacker.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onEntityDeath(EntityDeathEvent event) {
|
||||
if (bridge == null) return;
|
||||
Entity entity = event.getEntity();
|
||||
Player killer = event.getEntity().getKiller();
|
||||
|
||||
if (killer != null && entity instanceof Player victim) {
|
||||
bridge.incrementEvent("combat.player_kill");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.server.ServerCommandEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 命令事件监听器 — 监控所有玩家和后台命令执行
|
||||
*
|
||||
* 实时检测:
|
||||
* - CommandAbuse(命令滥用)— OP 权限的敏感命令使用
|
||||
* - 插件后门命令 — 异常插件注册的危险命令
|
||||
* - 命令注入 — 包含 shell 特殊字符的命令
|
||||
*/
|
||||
public class CommandEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CommandEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** 高危命令前缀列表(需要告警监控) */
|
||||
private static final String[] SENSITIVE_COMMANDS = {
|
||||
"op ", "deop ", "ban ", "pardon ", "whitelist ", "stop", "restart",
|
||||
"reload", "plugins", "version", "about", "icanhasbukkit",
|
||||
"save-all", "save-on", "save-off", "difficulty", "defaultgamemode",
|
||||
"seed", "worldborder", "spreadplayers", "fill", "clone",
|
||||
"execute", "function", "datapack", "gamerule", "kill",
|
||||
"summon", "give", "clear", "enchant", "effect",
|
||||
"tp ", "teleport ", "gamemode ", "kick "
|
||||
};
|
||||
|
||||
public CommandEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
|
||||
if (bridge == null) return;
|
||||
String command = event.getMessage().toLowerCase().trim();
|
||||
String playerName = event.getPlayer().getName();
|
||||
PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId());
|
||||
|
||||
bridge.incrementEvent("command.player");
|
||||
|
||||
// 记录最近命令(用于滥用检测)
|
||||
if (snap != null) {
|
||||
snap.recentCommands.add(command);
|
||||
while (snap.recentCommands.size() > 10) {
|
||||
snap.recentCommands.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
// —— 敏感命令监控 ——
|
||||
for (String sensitive : SENSITIVE_COMMANDS) {
|
||||
if (command.startsWith("/" + sensitive) || command.startsWith("minecraft:" + sensitive)) {
|
||||
bridge.alert(AlertType.COMMAND_ABUSE,
|
||||
String.format("玩家 %s 执行敏感命令:%s", playerName, event.getMessage()),
|
||||
0.6, playerName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// —— 命令注入检测 ——
|
||||
if (command.contains("${") || command.contains("`") ||
|
||||
command.contains("&&") || command.contains("||") ||
|
||||
command.contains(";") && command.contains("rm ")) {
|
||||
bridge.alert(AlertType.COMMAND_ABUSE,
|
||||
String.format("玩家 %s 命令疑似注入:%s", playerName, event.getMessage()),
|
||||
0.9, playerName);
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onServerCommand(ServerCommandEvent event) {
|
||||
if (bridge == null) return;
|
||||
bridge.incrementEvent("command.server");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.*;
|
||||
import org.bukkit.event.player.PlayerFishEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 实体事件监听器 — 处理实体生成、拾取、交互
|
||||
*
|
||||
* 实时检测:
|
||||
* - AutoFish(自动钓鱼)— 异常快速的钓鱼收杆时机
|
||||
* - 实体生成速率异常(可能为故意的服务器压力测试)
|
||||
*/
|
||||
public class EntityEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EntityEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
public EntityEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onEntitySpawn(EntitySpawnEvent event) {
|
||||
if (bridge == null) return;
|
||||
bridge.incrementEvent("entity.spawn");
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerFish(PlayerFishEvent event) {
|
||||
if (bridge == null) return;
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// —— AutoFish 检测 ——
|
||||
// 正常钓鱼需要玩家注意力响应,AutoFish hack 会在鱼咬钩瞬间自动收杆
|
||||
if (event.getState() == PlayerFishEvent.State.CAUGHT_FISH
|
||||
|| event.getState() == PlayerFishEvent.State.CAUGHT_ENTITY) {
|
||||
|
||||
bridge.incrementEvent("fish.caught");
|
||||
|
||||
// 记录钓鱼收杆时间,如果连续快速收杆则标记
|
||||
var snap = bridge.getPlayer(player.getUniqueId());
|
||||
if (snap != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
long recentCatches = snap.recentTargets.values().stream()
|
||||
.filter(t -> t > now - 30_000) // 30秒内
|
||||
.count();
|
||||
if (recentCatches > 5) { // 30秒内收杆超过5次(正常钓鱼约15-60秒一次)
|
||||
bridge.alert(AlertType.SECURITY_AUTO_FISH,
|
||||
String.format("玩家 %s 30秒内钓鱼收杆 %d 次,疑似 AutoFish",
|
||||
player.getName(), recentCatches),
|
||||
0.7, player.getName());
|
||||
}
|
||||
// 复用 recentTargets 的 key 为 "fish" 来记录钓鱼时间
|
||||
snap.recentTargets.put("fish:" + System.nanoTime(), now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onEntityPickupItem(EntityPickupItemEvent event) {
|
||||
if (bridge == null) return;
|
||||
if (event.getEntity() instanceof Player) {
|
||||
bridge.incrementEvent("entity.pickup");
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onEntityExplode(EntityExplodeEvent event) {
|
||||
if (bridge == null) return;
|
||||
bridge.incrementEvent("entity.explode");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.*;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 物品栏事件监听器 — 处理容器交互、物品移动
|
||||
*
|
||||
* 实时检测:
|
||||
* - ChestSteal(快速偷箱)— 短时间内从容器取出大量物品
|
||||
* - InventoryManipulation(物品栏操控)— 异常快速的物品移动模式
|
||||
* - Dupe(物品复制)— 异常物品数量变化
|
||||
*/
|
||||
public class InventoryEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(InventoryEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** 单次 Click 事件中物品数量变化的阈值(超过视为可疑) */
|
||||
private static final int ITEM_CHANGE_THRESHOLD = 64;
|
||||
|
||||
public InventoryEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (bridge == null) return;
|
||||
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||
|
||||
Inventory inv = event.getInventory();
|
||||
InventoryType type = inv.getType();
|
||||
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
|
||||
if (snap == null) return;
|
||||
|
||||
bridge.incrementEvent("inventory.click");
|
||||
|
||||
// —— ChestSteal 检测 ——
|
||||
// 检测玩家在短时间内从他人容器中快速取走物品
|
||||
if (type == InventoryType.CHEST || type == InventoryType.BARREL
|
||||
|| type == InventoryType.SHULKER_BOX || type == InventoryType.ENDER_CHEST) {
|
||||
|
||||
ItemStack current = event.getCurrentItem();
|
||||
ItemStack cursor = event.getCursor();
|
||||
|
||||
// 快速移动(Shift-Click)整组物品
|
||||
if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY && current != null) {
|
||||
int movedAmount = current.getAmount();
|
||||
if (movedAmount >= ITEM_CHANGE_THRESHOLD / 2) {
|
||||
bridge.alert(AlertType.SECURITY_CHEST_STEAL,
|
||||
String.format("玩家 %s 从容器快速取走 %d 个物品",
|
||||
player.getName(), movedAmount),
|
||||
0.5, player.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onInventoryOpen(InventoryOpenEvent event) {
|
||||
if (bridge == null) return;
|
||||
if (!(event.getPlayer() instanceof Player player)) return;
|
||||
|
||||
bridge.incrementEvent("inventory.open");
|
||||
|
||||
// 记录容器打开以辅助 ChestSteal 上下文判断
|
||||
Inventory inv = event.getInventory();
|
||||
if (inv.getHolder() instanceof Container container) {
|
||||
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
|
||||
if (snap != null) {
|
||||
// 记录容器位置用于后续分析
|
||||
String locKey = String.format("container:%d,%d,%d",
|
||||
container.getLocation().getBlockX(),
|
||||
container.getLocation().getBlockY(),
|
||||
container.getLocation().getBlockZ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (bridge == null) return;
|
||||
if (!(event.getPlayer() instanceof Player)) return;
|
||||
bridge.incrementEvent("inventory.close");
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (bridge == null) return;
|
||||
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||
bridge.incrementEvent("inventory.drag");
|
||||
|
||||
// 大量物品拖动(可能为 InventoryManipulation hack)
|
||||
int totalSlots = event.getInventorySlots().size();
|
||||
if (totalSlots > 16) {
|
||||
bridge.alert(AlertType.SECURITY_INVENTORY_MANIPULATION,
|
||||
String.format("玩家 %s 一次拖动覆盖 %d 个物品栏位,疑似 InventoryManipulation",
|
||||
player.getName(), totalSlots),
|
||||
0.65, player.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 包级事件监听器 — 利用 Paper API 对网络包进行细粒度监控
|
||||
*
|
||||
* Paper 特有 API,非 Paper 服务端会自动跳过注册。
|
||||
*
|
||||
* 实时检测:
|
||||
* - PacketFlood(包洪水)— 单个玩家发送异常数量的包
|
||||
* - TabCompleteCrash — 异常大的 TabComplete 请求
|
||||
* - BookBan — 超大书内容导致的崩溃包
|
||||
* - ResourcePackExploit — 恶意资源包下载尝试
|
||||
*/
|
||||
public class PacketEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PacketEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** 每秒最大允许的包速率(单玩家) */
|
||||
private static final int MAX_PACKETS_PER_SECOND = 200;
|
||||
|
||||
public PacketEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 Paper 的包预接收事件(需 Paper 1.20.6+)
|
||||
* 使用反射调用避免编译时硬依赖 Paper 特定 API 类
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPacketReceive(com.destroystokyo.paper.event.player.PlayerHandshakeEvent event) {
|
||||
if (bridge == null) return;
|
||||
// 握手阶段即可获取连接来源
|
||||
bridge.incrementEvent("packet.handshake");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的包事件监听 — 使用 Paper 1.21+ 的 AsyncPacketReceiveEvent
|
||||
* 注:此类在 paper-api 1.21 中可用,但具体 API 可能因版本而异
|
||||
*/
|
||||
// @EventHandler 注释留空,待 Paper 版本确认后启用
|
||||
// 当前通过 DataBridge.recordPacket() 提供包速率追踪入口
|
||||
public void onAsyncPacket(Object event) {
|
||||
if (bridge == null) return;
|
||||
bridge.recordPacket();
|
||||
bridge.incrementEvent("packet.total");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.player.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 玩家事件监听器 — 处理登录、退出、移动、传送、死亡等核心玩家事件
|
||||
*
|
||||
* 实时检测:
|
||||
* - Speed(移动速度异常)— 水平速度超过 0.65 blocks/tick 则标记
|
||||
* - Fly(非法飞行)— 非创造/旁观模式下,垂直位移模式异常
|
||||
* - Jesus(水行)— 连续在水中但 y 坐标不下降
|
||||
* - NoFall(无摔伤)— 从高处落地无伤害
|
||||
*/
|
||||
public class PlayerEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PlayerEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** 最大允许水平速度(blocks/tick),超过此值视为 Speed Hack */
|
||||
private static final double MAX_HORIZONTAL_SPEED = 0.65;
|
||||
|
||||
/** 可疑移动的告警置信度阈值 */
|
||||
private static final double SUSPICIOUS_CONFIDENCE = 0.75;
|
||||
|
||||
public PlayerEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
// ─── 登录/退出 ──────────────────────────────────────
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
if (bridge == null) return;
|
||||
Player player = event.getPlayer();
|
||||
bridge.registerPlayer(player);
|
||||
bridge.incrementEvent("player.join");
|
||||
|
||||
// 检查同 IP 多账号
|
||||
String ip = player.getAddress() != null && player.getAddress().getAddress() != null
|
||||
? player.getAddress().getAddress().getHostAddress()
|
||||
: "unknown";
|
||||
var sameIP = bridge.getPlayersByIP(ip);
|
||||
if (sameIP.size() > 2) {
|
||||
bridge.alert(AlertType.SECURITY_ALT_ACCOUNT,
|
||||
String.format("玩家 %s 使用IP %s 登录,该IP已有 %d 个在线账号",
|
||||
player.getName(), ip, sameIP.size()),
|
||||
0.7, player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
if (bridge == null) return;
|
||||
bridge.removePlayer(event.getPlayer().getUniqueId());
|
||||
bridge.incrementEvent("player.quit");
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerKick(PlayerKickEvent event) {
|
||||
if (bridge == null) return;
|
||||
bridge.removePlayer(event.getPlayer().getUniqueId());
|
||||
bridge.incrementEvent("player.kick");
|
||||
}
|
||||
|
||||
// ─── 移动检测 ──────────────────────────────────────
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerMove(PlayerMoveEvent event) {
|
||||
if (bridge == null) return;
|
||||
Player player = event.getPlayer();
|
||||
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
|
||||
if (snap == null) return;
|
||||
|
||||
Location from = event.getFrom();
|
||||
Location to = event.getTo();
|
||||
if (to == null) return;
|
||||
|
||||
// 仅在位置实际改变时更新
|
||||
if (from.getX() == to.getX() && from.getY() == to.getY() && from.getZ() == to.getZ()) return;
|
||||
|
||||
// 更新快照位置
|
||||
snap.updatePosition(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch());
|
||||
|
||||
// 计算水平移动距离
|
||||
double dx = to.getX() - from.getX();
|
||||
double dz = to.getZ() - from.getZ();
|
||||
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
|
||||
double dy = to.getY() - from.getY();
|
||||
|
||||
snap.totalDistanceMoved += horizontalDist;
|
||||
|
||||
// —— Speed 检测 ——
|
||||
// 排除创造/旁观模式、鞘翅飞行、骑乘状态
|
||||
if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL
|
||||
&& !player.isGliding()
|
||||
&& !player.isInsideVehicle()
|
||||
&& !player.isFlying()) {
|
||||
|
||||
if (horizontalDist > MAX_HORIZONTAL_SPEED) {
|
||||
bridge.alert(AlertType.SECURITY_SPEED,
|
||||
String.format("玩家 %s 移动速度异常:水平 %.3f blocks/tick(阈值 %.2f)",
|
||||
player.getName(), horizontalDist, MAX_HORIZONTAL_SPEED),
|
||||
Math.min(0.95, horizontalDist / MAX_HORIZONTAL_SPEED * 0.6),
|
||||
player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// —— Fly 检测 ——
|
||||
// 非飞行玩家在空中时持续 y 坐标下降过慢
|
||||
if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL
|
||||
&& !player.isFlying()
|
||||
&& !player.isGliding()
|
||||
&& !player.isInWater()
|
||||
&& !player.isInsideVehicle()) {
|
||||
|
||||
// 如果玩家在空中(脚下无方块)且 y 不下降反而上升
|
||||
boolean onGround = ((org.bukkit.entity.LivingEntity) player).isOnGround();
|
||||
if (!onGround && dy > 0.05 && horizontalDist < 0.1) {
|
||||
bridge.alert(AlertType.SECURITY_FLY,
|
||||
String.format("玩家 %s 疑似飞行:空中上升 dy=%.3f", player.getName(), dy),
|
||||
SUSPICIOUS_CONFIDENCE, player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// —— Jesus(水行)检测 ——
|
||||
if (player.isInWater() && player.getGameMode() == org.bukkit.GameMode.SURVIVAL
|
||||
&& !player.isFlying() && !player.isSwimming()) {
|
||||
// 在水中但 y 坐标超过水面特征线
|
||||
if (dy > -0.01 && horizontalDist > 0.3) {
|
||||
bridge.alert(AlertType.SECURITY_JESUS,
|
||||
String.format("玩家 %s 疑似水行:水中水平移动 %.3f blocks", player.getName(), horizontalDist),
|
||||
0.65, player.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 传送跟踪 ──────────────────────────────────────
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerTeleport(PlayerTeleportEvent event) {
|
||||
if (bridge == null) return;
|
||||
PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId());
|
||||
if (snap != null && event.getTo() != null) {
|
||||
snap.updatePosition(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(),
|
||||
event.getTo().getYaw(), event.getTo().getPitch());
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 摔伤检测(NoFall) ─────────────────────────────
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerDamage(EntityDamageEvent event) {
|
||||
if (bridge == null) return;
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
if (event.getCause() != EntityDamageEvent.DamageCause.FALL) return;
|
||||
|
||||
PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
|
||||
if (snap == null) return;
|
||||
|
||||
double fallDistance = player.getFallDistance();
|
||||
// 正常情况:掉落 > 3 方块高度应有伤害
|
||||
// NoFall hack 会让服务端认为掉落距离为 0
|
||||
if (fallDistance > 3.5 && event.getDamage() < 1.0) {
|
||||
bridge.alert(AlertType.SECURITY_NOFALL,
|
||||
String.format("玩家 %s 疑似 NoFall:掉落 %.1f 方块但伤害仅 %.1f",
|
||||
player.getName(), fallDistance, event.getDamage()),
|
||||
0.8, player.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.aluer.plugin.listener;
|
||||
|
||||
import com.aluer.plugin.AluerPlugin;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 世界事件监听器 — 处理区块加载/卸载、世界保存
|
||||
*
|
||||
* 主要用于性能监控:
|
||||
* - 区块加载速率(异常高可能表明 Xray 或 Baritone 矿透机器人)
|
||||
* - 世界保存耗时跟踪
|
||||
*/
|
||||
public class WorldEventListener implements Listener {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WorldEventListener.class);
|
||||
|
||||
private final AluerPlugin plugin;
|
||||
private final DataBridge bridge;
|
||||
|
||||
/** 区块加载速率告警阈值(每秒区块数),Baritone/Xray 会快速加载大量区块 */
|
||||
private static final int CHUNK_LOAD_RATE_WARNING = 30;
|
||||
|
||||
public WorldEventListener(AluerPlugin plugin, DataBridge bridge) {
|
||||
this.plugin = plugin;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onChunkLoad(ChunkLoadEvent event) {
|
||||
if (bridge == null) return;
|
||||
long count = bridge.incrementEvent("world.chunk_load");
|
||||
|
||||
// 高频区块加载可能表明 Baritone 矿透或 Xray 探索
|
||||
if (count > CHUNK_LOAD_RATE_WARNING) {
|
||||
// 每秒重置计数器(由 DataBridge 的调度处理)
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onChunkUnload(ChunkUnloadEvent event) {
|
||||
if (bridge == null) return;
|
||||
bridge.incrementEvent("world.chunk_unload");
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldSave(WorldSaveEvent event) {
|
||||
if (bridge == null) return;
|
||||
bridge.incrementEvent("world.save");
|
||||
logger.debug("World saved: {}", event.getWorld().getName());
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import com.aluer.ai.DeepSeekClient;
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import com.aluer.model.AlertEvent;
|
||||
import com.aluer.model.AlertType;
|
||||
import com.aluer.plugin.bridge.InternalCommandExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -16,16 +18,20 @@ import java.util.regex.Pattern;
|
||||
@Component
|
||||
public class AutoExecutor {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AutoExecutor.class);
|
||||
|
||||
|
||||
private final ServerGuardConfig config;
|
||||
private final RconClient rconClient;
|
||||
private final DeepSeekClient deepSeekClient;
|
||||
private final ConcurrentLinkedQueue<ExecuteTask> taskQueue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentHashMap<String, Long> recentActions = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
private static final Pattern IP_PATTERN = Pattern.compile("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b");
|
||||
private static final long ACTION_COOLDOWN_MS = 60000;
|
||||
|
||||
/** 插件模式下的内部命令执行器(external 模式下为 null) */
|
||||
@Autowired(required = false)
|
||||
private InternalCommandExecutor internalCommandExecutor;
|
||||
|
||||
public AutoExecutor(ServerGuardConfig config, RconClient rconClient, DeepSeekClient deepSeekClient) {
|
||||
this.config = config;
|
||||
this.rconClient = rconClient;
|
||||
@@ -86,13 +92,15 @@ public class AutoExecutor {
|
||||
logger.info("[AUTO-EXEC] Ban disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String ip = extractIpFromAlert(alert);
|
||||
|
||||
|
||||
if (ip != null) {
|
||||
String banKey = "ban-ip:" + ip;
|
||||
if (!isActionCooldown(banKey)) {
|
||||
boolean success = rconClient.banIp(ip);
|
||||
boolean success = isPluginMode()
|
||||
? internalCommandExecutor.banIP(ip, "AI 自动封禁 — " + alert.getType().getTitle())
|
||||
: rconClient.banIp(ip);
|
||||
logger.info("[AUTO-EXEC] Ban IP {} - {}", ip, success ? "SUCCESS" : "FAILED");
|
||||
recordAction(banKey);
|
||||
}
|
||||
@@ -101,7 +109,9 @@ public class AutoExecutor {
|
||||
if (player != null) {
|
||||
String banKey = "ban-player:" + player;
|
||||
if (!isActionCooldown(banKey)) {
|
||||
boolean success = rconClient.banPlayer(player);
|
||||
boolean success = isPluginMode()
|
||||
? internalCommandExecutor.banPlayer(player, "AI 自动封禁 — " + alert.getType().getTitle())
|
||||
: rconClient.banPlayer(player);
|
||||
logger.info("[AUTO-EXEC] Ban player {} - {}", player, success ? "SUCCESS" : "FAILED");
|
||||
recordAction(banKey);
|
||||
}
|
||||
@@ -113,12 +123,14 @@ public class AutoExecutor {
|
||||
if (!config.getAi().getDeepseek().getAutoExecute().isKickPlayer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String player = extractPlayerFromAlert(alert);
|
||||
if (player != null) {
|
||||
String kickKey = "kick:" + player;
|
||||
if (!isActionCooldown(kickKey)) {
|
||||
boolean success = rconClient.kickPlayer(player, "Auto-kick by AI security system");
|
||||
boolean success = isPluginMode()
|
||||
? internalCommandExecutor.kickPlayer(player, "AI 安全系统自动踢出")
|
||||
: rconClient.kickPlayer(player, "Auto-kick by AI security system");
|
||||
logger.info("[AUTO-EXEC] Kick player {} - {}", player, success ? "SUCCESS" : "FAILED");
|
||||
recordAction(kickKey);
|
||||
}
|
||||
@@ -129,14 +141,18 @@ public class AutoExecutor {
|
||||
if (!config.getAi().getDeepseek().getAutoExecute().isClearLag()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String clearKey = "clear-lag";
|
||||
if (!isActionCooldown(clearKey)) {
|
||||
boolean success = rconClient.clearLag();
|
||||
logger.info("[AUTO-EXEC] Clear lag - {}", success ? "SUCCESS" : "FAILED");
|
||||
if (isPluginMode()) {
|
||||
int removed = internalCommandExecutor.clearDroppedItems();
|
||||
logger.info("[AUTO-EXEC] Clear lag - removed {} dropped items via Bukkit API", removed);
|
||||
} else {
|
||||
boolean success = rconClient.clearLag();
|
||||
logger.info("[AUTO-EXEC] Clear lag - {}", success ? "SUCCESS" : "FAILED");
|
||||
rconClient.killAllMobs();
|
||||
}
|
||||
recordAction(clearKey);
|
||||
|
||||
rconClient.killAllMobs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +160,14 @@ public class AutoExecutor {
|
||||
if (!config.getAi().getDeepseek().getAutoExecute().isSetSpawnRate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String spawnKey = "spawn-control";
|
||||
if (!isActionCooldown(spawnKey)) {
|
||||
rconClient.setSpawnRate(5);
|
||||
if (isPluginMode()) {
|
||||
internalCommandExecutor.setSpawnRate(5);
|
||||
} else {
|
||||
rconClient.setSpawnRate(5);
|
||||
}
|
||||
logger.info("[AUTO-EXEC] Reduced spawn rate to prevent lag");
|
||||
recordAction(spawnKey);
|
||||
}
|
||||
@@ -157,14 +177,22 @@ public class AutoExecutor {
|
||||
if (!config.getAi().getDeepseek().getAutoExecute().isWhitelist()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String whitelistKey = "whitelist";
|
||||
if (!isActionCooldown(whitelistKey)) {
|
||||
if (action.contains("enable") || action.contains("开启") || action.contains("启用")) {
|
||||
rconClient.enableWhitelist();
|
||||
if (isPluginMode()) {
|
||||
internalCommandExecutor.enableWhitelist();
|
||||
} else {
|
||||
rconClient.enableWhitelist();
|
||||
}
|
||||
logger.info("[AUTO-EXEC] Enabled whitelist");
|
||||
} else if (action.contains("disable") || action.contains("关闭")) {
|
||||
rconClient.disableWhitelist();
|
||||
if (isPluginMode()) {
|
||||
internalCommandExecutor.disableWhitelist();
|
||||
} else {
|
||||
rconClient.disableWhitelist();
|
||||
}
|
||||
logger.info("[AUTO-EXEC] Disabled whitelist");
|
||||
}
|
||||
recordAction(whitelistKey);
|
||||
@@ -176,10 +204,15 @@ public class AutoExecutor {
|
||||
if (!isActionCooldown(restartKey)) {
|
||||
logger.warn("[AUTO-EXEC] AI recommended server restart");
|
||||
logger.warn("[AUTO-EXEC] Sending alert to admin before restart...");
|
||||
|
||||
rconClient.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues");
|
||||
rconClient.executeCommand("save-all");
|
||||
|
||||
|
||||
if (isPluginMode()) {
|
||||
internalCommandExecutor.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues");
|
||||
internalCommandExecutor.saveAllWorlds();
|
||||
} else {
|
||||
rconClient.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues");
|
||||
rconClient.executeCommand("save-all");
|
||||
}
|
||||
|
||||
recordAction(restartKey);
|
||||
}
|
||||
}
|
||||
@@ -187,7 +220,11 @@ public class AutoExecutor {
|
||||
private void executeWorldBorder() {
|
||||
String borderKey = "world-border";
|
||||
if (!isActionCooldown(borderKey)) {
|
||||
rconClient.setWorldBorder(1000);
|
||||
if (isPluginMode()) {
|
||||
internalCommandExecutor.executeCommand("worldborder set 1000");
|
||||
} else {
|
||||
rconClient.setWorldBorder(1000);
|
||||
}
|
||||
logger.info("[AUTO-EXEC] Set world border to reduce entity spawns");
|
||||
recordAction(borderKey);
|
||||
}
|
||||
@@ -195,13 +232,18 @@ public class AutoExecutor {
|
||||
|
||||
private String extractIpFromAlert(AlertEvent alert) {
|
||||
String message = alert.getMessage();
|
||||
|
||||
|
||||
Matcher matcher = IP_PATTERN.matcher(message);
|
||||
if (matcher.find()) {
|
||||
return matcher.group();
|
||||
}
|
||||
|
||||
return rconClient.extractIpFromMessage(message);
|
||||
|
||||
return isPluginMode() ? null : rconClient.extractIpFromMessage(message);
|
||||
}
|
||||
|
||||
/** 当前是否运行在 Paper 插件模式 */
|
||||
private boolean isPluginMode() {
|
||||
return config.isPluginMode() && internalCommandExecutor != null;
|
||||
}
|
||||
|
||||
private String extractPlayerFromAlert(AlertEvent alert) {
|
||||
@@ -255,13 +297,25 @@ public class AutoExecutor {
|
||||
ExecuteTask task;
|
||||
while ((task = taskQueue.poll()) != null) {
|
||||
try {
|
||||
switch (task.getType()) {
|
||||
case BAN_IP -> rconClient.banIp(task.getTarget());
|
||||
case BAN_PLAYER -> rconClient.banPlayer(task.getTarget());
|
||||
case KICK -> rconClient.kickPlayer(task.getTarget(), task.getReason());
|
||||
case CLEAR_LAG -> rconClient.clearLag();
|
||||
case RESTART -> rconClient.restartServer();
|
||||
default -> logger.warn("Unknown task type: {}", task.getType());
|
||||
if (isPluginMode()) {
|
||||
switch (task.getType()) {
|
||||
case BAN_IP -> internalCommandExecutor.banIP(task.getTarget(), task.getReason());
|
||||
case BAN_PLAYER -> internalCommandExecutor.banPlayer(task.getTarget(), task.getReason());
|
||||
case KICK -> internalCommandExecutor.kickPlayer(task.getTarget(), task.getReason());
|
||||
case CLEAR_LAG -> internalCommandExecutor.clearDroppedItems();
|
||||
case RESTART -> internalCommandExecutor.executeCommand("restart");
|
||||
case WHITELIST -> internalCommandExecutor.enableWhitelist();
|
||||
default -> logger.warn("Unknown task type: {}", task.getType());
|
||||
}
|
||||
} else {
|
||||
switch (task.getType()) {
|
||||
case BAN_IP -> rconClient.banIp(task.getTarget());
|
||||
case BAN_PLAYER -> rconClient.banPlayer(task.getTarget());
|
||||
case KICK -> rconClient.kickPlayer(task.getTarget(), task.getReason());
|
||||
case CLEAR_LAG -> rconClient.clearLag();
|
||||
case RESTART -> rconClient.restartServer();
|
||||
default -> logger.warn("Unknown task type: {}", task.getType());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to execute task: {}", e.getMessage());
|
||||
|
||||
@@ -6,8 +6,11 @@ import com.aluer.config.ServerGuardConfig;
|
||||
import com.aluer.monitor.*;
|
||||
import com.aluer.model.AlertEvent;
|
||||
import com.aluer.model.MetricsData;
|
||||
import com.aluer.plugin.bridge.DataBridge;
|
||||
import com.aluer.plugin.bridge.InternalCommandExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -36,6 +39,14 @@ public class ServerGuardService implements CommandLineRunner {
|
||||
private final AutoExecutor autoExecutor;
|
||||
private final RconClient rconClient;
|
||||
|
||||
/** 插件模式下的数据桥接(external 模式下为 null) */
|
||||
@Autowired(required = false)
|
||||
private DataBridge dataBridge;
|
||||
|
||||
/** 插件模式下的内部命令执行器(external 模式下为 null) */
|
||||
@Autowired(required = false)
|
||||
private InternalCommandExecutor internalCommandExecutor;
|
||||
|
||||
@Value("${server.port:8080}")
|
||||
private int serverPort;
|
||||
|
||||
@@ -84,6 +95,20 @@ public class ServerGuardService implements CommandLineRunner {
|
||||
|
||||
startMonitoring();
|
||||
|
||||
// 插件模式:注册 DataBridge 告警/指标处理器
|
||||
if (config.isPluginMode() && dataBridge != null) {
|
||||
dataBridge.setAlertHandler(this::handleAlert);
|
||||
dataBridge.setMetricsHandler(metrics -> {
|
||||
deepSeekClient.addMetrics(metrics);
|
||||
adaptiveThreshold.addMetric(metrics);
|
||||
anomalyDetector.addMetric(metrics);
|
||||
timeSeriesPredictor.addMetric(metrics);
|
||||
});
|
||||
// 启动 DataBridge 包速率定时更新
|
||||
scheduler.scheduleAtFixedRate(() -> dataBridge.updatePacketRate(), 1, 1, TimeUnit.SECONDS);
|
||||
logger.info("DataBridge alert/metrics handlers registered — plugin mode active");
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
logger.info("Shutdown signal received, gracefully stopping...");
|
||||
running.set(false);
|
||||
@@ -107,8 +132,9 @@ public class ServerGuardService implements CommandLineRunner {
|
||||
private void printBanner() {
|
||||
logger.info("");
|
||||
logger.info("╔══════════════════════════════════════════════════════════════╗");
|
||||
logger.info("║ Aluer ServerGuard v1.0.0 ║");
|
||||
logger.info("║ Aluer ServerGuard v4.0.0 ║");
|
||||
logger.info("║ AI-Powered Minecraft Server Protection ║");
|
||||
logger.info("║ Mode: " + String.format("%-39s", config.isPluginMode() ? "Paper Plugin (Embedded)" : "External Monitor") + "║");
|
||||
logger.info("╠══════════════════════════════════════════════════════════════╣");
|
||||
logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version")));
|
||||
logger.info("║ Available CPUs: {}{} ║",
|
||||
@@ -210,6 +236,15 @@ public class ServerGuardService implements CommandLineRunner {
|
||||
|
||||
private void collectAndAnalyzeMetrics() {
|
||||
try {
|
||||
// 插件模式:从 DataBridge 采集实时指标
|
||||
if (config.isPluginMode() && dataBridge != null) {
|
||||
MetricsData pluginMetrics = dataBridge.buildMetricsSnapshot();
|
||||
deepSeekClient.addMetrics(pluginMetrics);
|
||||
adaptiveThreshold.addMetric(pluginMetrics);
|
||||
anomalyDetector.addMetric(pluginMetrics);
|
||||
timeSeriesPredictor.addMetric(pluginMetrics);
|
||||
}
|
||||
|
||||
MetricsData data = resourceMonitor.collectMetrics();
|
||||
|
||||
deepSeekClient.addMetrics(data);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
serverguard:
|
||||
# 运行模式:plugin(Paper 插件嵌入式)或 external(传统外部监控)
|
||||
# Paper 插件模式由系统属性 serverguard.mode=plugin 自动设置
|
||||
mode: ${SERVERGUARD_MODE:external}
|
||||
|
||||
minecraft:
|
||||
service-name: minecraft
|
||||
process-name: paper-1.21.11.jar
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: AluerServerGuard
|
||||
version: 4.0.0
|
||||
main: com.aluer.plugin.AluerPlugin
|
||||
api-version: '1.21'
|
||||
description: AI-powered Minecraft PaperMC Server Protection System — 29 anti-cheat modules, DDoS defense, WAF, kernel-based threat fusion
|
||||
author: Aluer
|
||||
website: https://github.com/aluer-ai/serverguard
|
||||
load: STARTUP
|
||||
prefix: Aluer
|
||||
|
||||
commands:
|
||||
aluer:
|
||||
description: Aluer ServerGuard 主控命令
|
||||
usage: /aluer <status|reload|scan|defense|info>
|
||||
permission: aluer.admin
|
||||
aluerstatus:
|
||||
description: 查看 Aluer 实时防护状态
|
||||
usage: /aluerstatus
|
||||
permission: aluer.status
|
||||
aluerplayers:
|
||||
description: 查看玩家风险评分列表
|
||||
usage: /aluerplayers
|
||||
permission: aluer.status
|
||||
aluerblock:
|
||||
description: 手动封禁玩家/IP
|
||||
usage: /aluerblock <player|ip> <target> [reason]
|
||||
permission: aluer.admin
|
||||
aluerunblock:
|
||||
description: 手动解封玩家/IP
|
||||
usage: /aluerunblock <player|ip> <target>
|
||||
permission: aluer.admin
|
||||
aluerscan:
|
||||
description: 手动触发安全扫描
|
||||
usage: /aluerscan <player>
|
||||
permission: aluer.admin
|
||||
aluerwhitelist:
|
||||
description: 启用/禁用紧急白名单模式
|
||||
usage: /aluerwhitelist <on|off|status>
|
||||
permission: aluer.admin
|
||||
|
||||
permissions:
|
||||
aluer.admin:
|
||||
description: Aluer ServerGuard 管理员权限
|
||||
default: op
|
||||
aluer.status:
|
||||
description: 查看 Aluer 状态
|
||||
default: op
|
||||
Reference in New Issue
Block a user