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:
Peijun Zhao
2026-05-16 15:50:48 +08:00
parent 1269d4dfa5
commit 030ced87f9
22 changed files with 2454 additions and 48 deletions
+54 -12
View File
@@ -26,8 +26,16 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<smile.version>2.6.0</smile.version> <smile.version>2.6.0</smile.version>
<commons-math3.version>3.6.1</commons-math3.version> <commons-math3.version>3.6.1</commons-math3.version>
<paper.version>1.21.1-R0.1-SNAPSHOT</paper.version>
</properties> </properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -105,23 +113,19 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- PaperMC API for plugin mode (provided by Paper server at runtime) -->
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${paper.version}</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<finalName>serverguard</finalName> <finalName>serverguard</finalName>
<plugins> <plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
@@ -131,6 +135,44 @@
<target>17</target> <target>17</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.aluer.ServerGuardApplication</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>
@@ -7,6 +7,15 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
/**
* Aluer ServerGuard 主应用入口
*
* 支持两种运行模式:
* 1. standalone — java -jar serverguard.jar(传统外部监控模式)
* 2. plugin — 由 PaperMC 插件 AluerPlugin 调用 bootstrap() 启动嵌入式上下文
*
* 模式由系统属性 serverguard.mode 决定,AluerPlugin 启动时自动设置为 "plugin"
*/
@SpringBootApplication @SpringBootApplication
@EnableAsync @EnableAsync
public class ServerGuardApplication { public class ServerGuardApplication {
@@ -14,6 +23,8 @@ public class ServerGuardApplication {
private static volatile ConfigurableApplicationContext context; private static volatile ConfigurableApplicationContext context;
public static void main(String[] args) { public static void main(String[] args) {
// standalone 模式:直接启动
System.setProperty("serverguard.mode", "external");
context = SpringApplication.run(ServerGuardApplication.class, args); context = SpringApplication.run(ServerGuardApplication.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
@@ -23,7 +34,25 @@ public class ServerGuardApplication {
} }
}, "serverguard-shutdown-hook")); }, "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() { public static ConfigurableApplicationContext getContext() {
@@ -23,6 +23,13 @@ public class ServerGuardConfig {
private AnnouncementConfig announcement = new AnnouncementConfig(); private AnnouncementConfig announcement = new AnnouncementConfig();
private AfkConfig afk = new AfkConfig(); 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 { public static class MinecraftConfig {
private String serviceName = "minecraft"; private String serviceName = "minecraft";
private String processName = "java"; private String processName = "java";
@@ -1,6 +1,8 @@
package com.aluer.model; package com.aluer.model;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
public class AlertEvent { public class AlertEvent {
private AlertType type; private AlertType type;
@@ -9,6 +11,8 @@ public class AlertEvent {
private LocalDateTime timestamp; private LocalDateTime timestamp;
private double confidence; private double confidence;
private String suggestedAction; private String suggestedAction;
/** 告警来源(玩家名/IP/系统模块名) */
private String source;
public AlertEvent() { public AlertEvent() {
this.timestamp = LocalDateTime.now(); this.timestamp = LocalDateTime.now();
@@ -32,4 +36,11 @@ public class AlertEvent {
public void setConfidence(double confidence) { this.confidence = confidence; } public void setConfidence(double confidence) { this.confidence = confidence; }
public String getSuggestedAction() { return suggestedAction; } public String getSuggestedAction() { return suggestedAction; }
public void setSuggestedAction(String suggestedAction) { this.suggestedAction = 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());
}
} }
+52 -1
View File
@@ -1,6 +1,7 @@
package com.aluer.model; package com.aluer.model;
public enum AlertType { public enum AlertType {
// ─── 系统监控 ──────────────────────────────
PROCESS_DEAD("Process Dead", "Minecraft process not found"), PROCESS_DEAD("Process Dead", "Minecraft process not found"),
TPS_LOW("Low TPS", "TPS below threshold"), TPS_LOW("Low TPS", "TPS below threshold"),
CPU_HIGH("High CPU", "CPU usage above threshold"), CPU_HIGH("High CPU", "CPU usage above threshold"),
@@ -8,7 +9,57 @@ public enum AlertType {
CONNECTION_FLOOD("Connection Flood", "Possible DDoS attack detected"), CONNECTION_FLOOD("Connection Flood", "Possible DDoS attack detected"),
LOG_ATTACK("Attack Detected", "Malicious activity in logs"), LOG_ATTACK("Attack Detected", "Malicious activity in logs"),
BACKUP_FAILED("Backup Failed", "Backup task failed"), 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 title;
private final String description; 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.config.ServerGuardConfig;
import com.aluer.model.AlertEvent; import com.aluer.model.AlertEvent;
import com.aluer.model.AlertType; import com.aluer.model.AlertType;
import com.aluer.plugin.bridge.InternalCommandExecutor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -16,16 +18,20 @@ import java.util.regex.Pattern;
@Component @Component
public class AutoExecutor { public class AutoExecutor {
private static final Logger logger = LoggerFactory.getLogger(AutoExecutor.class); private static final Logger logger = LoggerFactory.getLogger(AutoExecutor.class);
private final ServerGuardConfig config; private final ServerGuardConfig config;
private final RconClient rconClient; private final RconClient rconClient;
private final DeepSeekClient deepSeekClient; private final DeepSeekClient deepSeekClient;
private final ConcurrentLinkedQueue<ExecuteTask> taskQueue = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<ExecuteTask> taskQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentHashMap<String, Long> recentActions = new ConcurrentHashMap<>(); 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 Pattern IP_PATTERN = Pattern.compile("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b");
private static final long ACTION_COOLDOWN_MS = 60000; private static final long ACTION_COOLDOWN_MS = 60000;
/** 插件模式下的内部命令执行器(external 模式下为 null) */
@Autowired(required = false)
private InternalCommandExecutor internalCommandExecutor;
public AutoExecutor(ServerGuardConfig config, RconClient rconClient, DeepSeekClient deepSeekClient) { public AutoExecutor(ServerGuardConfig config, RconClient rconClient, DeepSeekClient deepSeekClient) {
this.config = config; this.config = config;
this.rconClient = rconClient; this.rconClient = rconClient;
@@ -86,13 +92,15 @@ public class AutoExecutor {
logger.info("[AUTO-EXEC] Ban disabled in config"); logger.info("[AUTO-EXEC] Ban disabled in config");
return; return;
} }
String ip = extractIpFromAlert(alert); String ip = extractIpFromAlert(alert);
if (ip != null) { if (ip != null) {
String banKey = "ban-ip:" + ip; String banKey = "ban-ip:" + ip;
if (!isActionCooldown(banKey)) { 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"); logger.info("[AUTO-EXEC] Ban IP {} - {}", ip, success ? "SUCCESS" : "FAILED");
recordAction(banKey); recordAction(banKey);
} }
@@ -101,7 +109,9 @@ public class AutoExecutor {
if (player != null) { if (player != null) {
String banKey = "ban-player:" + player; String banKey = "ban-player:" + player;
if (!isActionCooldown(banKey)) { 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"); logger.info("[AUTO-EXEC] Ban player {} - {}", player, success ? "SUCCESS" : "FAILED");
recordAction(banKey); recordAction(banKey);
} }
@@ -113,12 +123,14 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isKickPlayer()) { if (!config.getAi().getDeepseek().getAutoExecute().isKickPlayer()) {
return; return;
} }
String player = extractPlayerFromAlert(alert); String player = extractPlayerFromAlert(alert);
if (player != null) { if (player != null) {
String kickKey = "kick:" + player; String kickKey = "kick:" + player;
if (!isActionCooldown(kickKey)) { 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"); logger.info("[AUTO-EXEC] Kick player {} - {}", player, success ? "SUCCESS" : "FAILED");
recordAction(kickKey); recordAction(kickKey);
} }
@@ -129,14 +141,18 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isClearLag()) { if (!config.getAi().getDeepseek().getAutoExecute().isClearLag()) {
return; return;
} }
String clearKey = "clear-lag"; String clearKey = "clear-lag";
if (!isActionCooldown(clearKey)) { if (!isActionCooldown(clearKey)) {
boolean success = rconClient.clearLag(); if (isPluginMode()) {
logger.info("[AUTO-EXEC] Clear lag - {}", success ? "SUCCESS" : "FAILED"); 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); recordAction(clearKey);
rconClient.killAllMobs();
} }
} }
@@ -144,10 +160,14 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isSetSpawnRate()) { if (!config.getAi().getDeepseek().getAutoExecute().isSetSpawnRate()) {
return; return;
} }
String spawnKey = "spawn-control"; String spawnKey = "spawn-control";
if (!isActionCooldown(spawnKey)) { 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"); logger.info("[AUTO-EXEC] Reduced spawn rate to prevent lag");
recordAction(spawnKey); recordAction(spawnKey);
} }
@@ -157,14 +177,22 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isWhitelist()) { if (!config.getAi().getDeepseek().getAutoExecute().isWhitelist()) {
return; return;
} }
String whitelistKey = "whitelist"; String whitelistKey = "whitelist";
if (!isActionCooldown(whitelistKey)) { if (!isActionCooldown(whitelistKey)) {
if (action.contains("enable") || action.contains("开启") || action.contains("启用")) { if (action.contains("enable") || action.contains("开启") || action.contains("启用")) {
rconClient.enableWhitelist(); if (isPluginMode()) {
internalCommandExecutor.enableWhitelist();
} else {
rconClient.enableWhitelist();
}
logger.info("[AUTO-EXEC] Enabled whitelist"); logger.info("[AUTO-EXEC] Enabled whitelist");
} else if (action.contains("disable") || action.contains("关闭")) { } else if (action.contains("disable") || action.contains("关闭")) {
rconClient.disableWhitelist(); if (isPluginMode()) {
internalCommandExecutor.disableWhitelist();
} else {
rconClient.disableWhitelist();
}
logger.info("[AUTO-EXEC] Disabled whitelist"); logger.info("[AUTO-EXEC] Disabled whitelist");
} }
recordAction(whitelistKey); recordAction(whitelistKey);
@@ -176,10 +204,15 @@ public class AutoExecutor {
if (!isActionCooldown(restartKey)) { if (!isActionCooldown(restartKey)) {
logger.warn("[AUTO-EXEC] AI recommended server restart"); logger.warn("[AUTO-EXEC] AI recommended server restart");
logger.warn("[AUTO-EXEC] Sending alert to admin before restart..."); logger.warn("[AUTO-EXEC] Sending alert to admin before restart...");
rconClient.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues"); if (isPluginMode()) {
rconClient.executeCommand("save-all"); 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); recordAction(restartKey);
} }
} }
@@ -187,7 +220,11 @@ public class AutoExecutor {
private void executeWorldBorder() { private void executeWorldBorder() {
String borderKey = "world-border"; String borderKey = "world-border";
if (!isActionCooldown(borderKey)) { 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"); logger.info("[AUTO-EXEC] Set world border to reduce entity spawns");
recordAction(borderKey); recordAction(borderKey);
} }
@@ -195,13 +232,18 @@ public class AutoExecutor {
private String extractIpFromAlert(AlertEvent alert) { private String extractIpFromAlert(AlertEvent alert) {
String message = alert.getMessage(); String message = alert.getMessage();
Matcher matcher = IP_PATTERN.matcher(message); Matcher matcher = IP_PATTERN.matcher(message);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(); 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) { private String extractPlayerFromAlert(AlertEvent alert) {
@@ -255,13 +297,25 @@ public class AutoExecutor {
ExecuteTask task; ExecuteTask task;
while ((task = taskQueue.poll()) != null) { while ((task = taskQueue.poll()) != null) {
try { try {
switch (task.getType()) { if (isPluginMode()) {
case BAN_IP -> rconClient.banIp(task.getTarget()); switch (task.getType()) {
case BAN_PLAYER -> rconClient.banPlayer(task.getTarget()); case BAN_IP -> internalCommandExecutor.banIP(task.getTarget(), task.getReason());
case KICK -> rconClient.kickPlayer(task.getTarget(), task.getReason()); case BAN_PLAYER -> internalCommandExecutor.banPlayer(task.getTarget(), task.getReason());
case CLEAR_LAG -> rconClient.clearLag(); case KICK -> internalCommandExecutor.kickPlayer(task.getTarget(), task.getReason());
case RESTART -> rconClient.restartServer(); case CLEAR_LAG -> internalCommandExecutor.clearDroppedItems();
default -> logger.warn("Unknown task type: {}", task.getType()); 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) { } catch (Exception e) {
logger.error("Failed to execute task: {}", e.getMessage()); logger.error("Failed to execute task: {}", e.getMessage());
@@ -6,8 +6,11 @@ import com.aluer.config.ServerGuardConfig;
import com.aluer.monitor.*; import com.aluer.monitor.*;
import com.aluer.model.AlertEvent; import com.aluer.model.AlertEvent;
import com.aluer.model.MetricsData; import com.aluer.model.MetricsData;
import com.aluer.plugin.bridge.DataBridge;
import com.aluer.plugin.bridge.InternalCommandExecutor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -36,6 +39,14 @@ public class ServerGuardService implements CommandLineRunner {
private final AutoExecutor autoExecutor; private final AutoExecutor autoExecutor;
private final RconClient rconClient; private final RconClient rconClient;
/** 插件模式下的数据桥接(external 模式下为 null) */
@Autowired(required = false)
private DataBridge dataBridge;
/** 插件模式下的内部命令执行器(external 模式下为 null) */
@Autowired(required = false)
private InternalCommandExecutor internalCommandExecutor;
@Value("${server.port:8080}") @Value("${server.port:8080}")
private int serverPort; private int serverPort;
@@ -84,6 +95,20 @@ public class ServerGuardService implements CommandLineRunner {
startMonitoring(); 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(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Shutdown signal received, gracefully stopping..."); logger.info("Shutdown signal received, gracefully stopping...");
running.set(false); running.set(false);
@@ -107,8 +132,9 @@ public class ServerGuardService implements CommandLineRunner {
private void printBanner() { private void printBanner() {
logger.info(""); logger.info("");
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("║ AI-Powered Minecraft Server Protection ║");
logger.info("║ Mode: " + String.format("%-39s", config.isPluginMode() ? "Paper Plugin (Embedded)" : "External Monitor") + "║");
logger.info("╠══════════════════════════════════════════════════════════════╣"); logger.info("╠══════════════════════════════════════════════════════════════╣");
logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version"))); logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version")));
logger.info("║ Available CPUs: {}{} ║", logger.info("║ Available CPUs: {}{} ║",
@@ -210,6 +236,15 @@ public class ServerGuardService implements CommandLineRunner {
private void collectAndAnalyzeMetrics() { private void collectAndAnalyzeMetrics() {
try { try {
// 插件模式:从 DataBridge 采集实时指标
if (config.isPluginMode() && dataBridge != null) {
MetricsData pluginMetrics = dataBridge.buildMetricsSnapshot();
deepSeekClient.addMetrics(pluginMetrics);
adaptiveThreshold.addMetric(pluginMetrics);
anomalyDetector.addMetric(pluginMetrics);
timeSeriesPredictor.addMetric(pluginMetrics);
}
MetricsData data = resourceMonitor.collectMetrics(); MetricsData data = resourceMonitor.collectMetrics();
deepSeekClient.addMetrics(data); deepSeekClient.addMetrics(data);
+4
View File
@@ -1,4 +1,8 @@
serverguard: serverguard:
# 运行模式:plugin(Paper 插件嵌入式)或 external(传统外部监控)
# Paper 插件模式由系统属性 serverguard.mode=plugin 自动设置
mode: ${SERVERGUARD_MODE:external}
minecraft: minecraft:
service-name: minecraft service-name: minecraft
process-name: paper-1.21.11.jar process-name: paper-1.21.11.jar
+47
View File
@@ -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