V5.0 安全模块大扩展:Meteor Client 全功能反作弊 + AI行为分析 + 服务器保护
新增 45 个安全模块,覆盖 Meteor Client 全部 hack 类别: 【反作弊基础扩展 6 模块】 - AntiTimerService: 游戏加速检测(移动tick间隔分析) - AntiVelocityService: 击退修改检测(伤害后速度分析) - AntiPhaseService: 穿墙检测(固体方块射线追踪) - AntiBlinkService: 闪烁检测(伤害-断连-重连时序) - AntiFastBreakService: 快速破坏检测(方块标准破坏时间对比) - AntiElytraFlyService: 鞘翅飞行检测(速度/高度操控分析) 【Meteor Combat 10 模块】 - AntiCriticalsService: 自动暴击检测(跳跃/地面状态验证) - AntiAutoCrystalService: 水晶Aura检测(放置/爆炸时序分析) - AntiAutoTotemService: 自动图腾检测(副手装备速度分析) - AntiSurroundService: 自动包围检测(脚下方块精确放置模式) - AntiAutoTrapService: 自动陷阱检测(目标周围方块网格分析) - AntiAutoArmorService: 自动装备检测(多槽位同tick交换) - AntiChestSwapService: 自动胸甲交换检测(鞘翅-胸甲乒乓切换) - AntiAutoLogService: 自动断线检测(伤害-断连500ms时序) - AntiHitboxesService: 扩展碰撞箱检测(射线-边界边缘命中率) - AntiBowAimBotService: 弓箭自瞄检测(移动目标精度分析) 【Meteor Movement 10 模块】 - AntiNoSlowService: 无减速检测(物品使用+移动速度组合) - AntiSpiderService: 爬墙检测(无攀爬方块垂直上升) - AntiStepService: 自动跨步检测(非跳跃1格上升) - AntiPacketFlyService: 数据包飞行检测(连续悬浮/弹跳模式) - AntiAirJumpService: 空中跳跃检测(离地跳跃包) - AntiLongJumpService: 远跳检测(跳跃后水平速度衰减分析) - AntiAntiHungerService: 免饥饿检测(活动量vs饥饿消耗率) - AntiFastFallService: 快速下落检测(超终端速度持续下落) - AntiVClipService: 垂直穿墙检测(固体方块间瞬移) - AntiNoFallService 增强: 新增GroundSpoof/数据包地面/坠落累积/瞬移坠落检测 【Meteor World+Player 10 模块】 - AntiSpeedMineService: 加速挖掘检测(方块标准破坏时间对比) - AntiFastUseService: 快速使用检测(物品冷却时间分析) - AntiNoInteractService: 交互绕过检测(持剑开箱等异常操作) - AntiAutoMineService: 自动挖掘检测(挖掘间隔方差/视角追踪) - AntiVeinMinerService: 连锁挖矿检测(相邻矿石快速序列) - AntiAutoToolService: 自动工具检测(同tick切工具+开挖) - AntiFakePlayerService: 假人检测(心跳响应/交互缺失/移动模式) - AntiPistonAuraService: 活塞光环检测(战斗上下文活塞激活率) - AntiAnchorService: 锚定检测(受击零位移+封闭空间) - AntiStashFinderService: 仓库扫描检测(网格区块加载+零交互) 【网络协议安全 5 模块】 - ProtocolStateValidator: 协议状态机验证(四态转换违规检测) - TokenBucketRateLimiter: 令牌桶速率限制器(可复用速率控制基元) - BotFingerprintDetector: 机器人指纹检测(五维行为指纹分析) - NBTExploitPrevention: NBT漏洞防护(深度/大小/书页/告示牌验证) - ConnectionHandshakeValidator: 握手完整性验证(协议版本/状态/主机名) 【服务器保护 4 模块】 - ChunkLoadRateLimiter: 区块加载限制器(WARN/LIMIT/BLOCK三级响应) - EntityCountEnforcer: 实体数量执行器(分块限制+优先级清理) - RedstoneUpdateLimiter: 红石更新限制器(三级渐进+观察者钟检测) - CrashExploitSignatureDB: 崩溃漏洞签名库(12种已知漏洞+4级自动响应) 【AI/ML 行为分析 4 模块】 - BehavioralProfilingEngine: 行为分析引擎(5维特征+香农熵+Z分数) - ThreatScoreAggregator: 威胁分数聚合器(指数衰减+4级上报) - MovementPatternAnalyzer: 移动模式分析器(FFT频谱+旋转分析) - CombatPatternRecognizer: 战斗模式识别器(CPS方差+多目标熵+扫视分析) 配置更新:AlertType新增39个枚举值,SuperEvolutionConfig新增39个开关, application.yml新增39个配置项。323项全量测试通过,0失败。
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
package com.aluer.security;
|
||||
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 区块加载速率限制器 — V5.0 服务器保护模块
|
||||
*
|
||||
* 检测原理:
|
||||
* Minecraft 区块加载是服务器最消耗 CPU 和内存的操作之一。正常玩家移动
|
||||
* 时每秒加载约 1-5 个区块(视距 8-12)。Xray 透视、自动化探索 Bot 或
|
||||
* 恶意玩家的区块轰炸可在单秒内加载 50+ 个区块,导致服务器严重卡顿甚至崩溃。
|
||||
*
|
||||
* 三类危险场景:
|
||||
* 1. Xray / 矿物透视 — 客户端通过快速遍历区块定位钻石/远古残骸等稀有矿物,
|
||||
* 需要极快速的区块加载来暴露地下结构。
|
||||
* 2. Chunk Ban 攻击 — 攻击者故意飞行到世界边缘(X/Z 超过 3000 万),
|
||||
* 利用极端坐标的区块生成计算量使服务器 OOM 或永久崩溃。
|
||||
* 3. 自动化探索 Bot — Baritone 等 AI 寻路程序在极短时间内覆盖大片区域,
|
||||
* 区块加载频率远超正常人类玩家。
|
||||
*
|
||||
* 三级梯次响应:
|
||||
* WARN (>20区块/秒) — 记录警告日志,继续观察
|
||||
* LIMIT (>40区块/秒) — 跳过部分区块加载请求,对玩家发送速率受限通知
|
||||
* BLOCK (>60区块/秒) — 完全阻止该玩家的区块加载,踢出玩家
|
||||
*
|
||||
* 特殊豁免:
|
||||
* - 出生点区块加载(登录瞬间的初始加载)
|
||||
* - 服务器内部区块加载(非玩家触发的世界生成/实体 AI)
|
||||
* - 管理员白名单玩家
|
||||
*
|
||||
* 配置开关:serverguard.security.super-evolution.chunk-load-rate-limiter
|
||||
*/
|
||||
@Service
|
||||
public class ChunkLoadRateLimiter {
|
||||
|
||||
private final ServerGuardConfig config;
|
||||
|
||||
/** 每个玩家的滑动窗口区块加载记录(保留最近 60 秒) */
|
||||
private final Map<String, List<ChunkLoadEvent>> playerLoadHistory = new ConcurrentHashMap<>();
|
||||
|
||||
/** 每个玩家当前所处的阶段(WARN / LIMIT / BLOCK) */
|
||||
private final Map<String, Stage> playerStage = new ConcurrentHashMap<>();
|
||||
|
||||
/** 每个玩家的限流冷却截止时间 */
|
||||
private final Map<String, Instant> playerCooldownUntil = new ConcurrentHashMap<>();
|
||||
|
||||
/** 每个世界的总区块加载计数(用于服务器健康监控) */
|
||||
private final Map<String, AtomicLong> worldChunkLoadTotal = new ConcurrentHashMap<>();
|
||||
|
||||
/** 极端坐标区块加载事件(X或Z超过30M),用于 chunk ban 检测 */
|
||||
private final Map<String, List<ExtremeCoordEvent>> extremeCoordEvents = new ConcurrentHashMap<>();
|
||||
|
||||
/** 总区块加载请求计数 */
|
||||
private final AtomicLong totalChunkLoads = new AtomicLong(0);
|
||||
/** 被阻止的区块加载计数 */
|
||||
private final AtomicLong blockedChunkLoads = new AtomicLong(0);
|
||||
/** chunk ban 攻击检测次数 */
|
||||
private final AtomicLong chunkBanDetections = new AtomicLong(0);
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
/** 区块加载速率阈值(区块/秒) */
|
||||
private static final int WARN_THRESHOLD = 20;
|
||||
private static final int LIMIT_THRESHOLD = 40;
|
||||
private static final int BLOCK_THRESHOLD = 60;
|
||||
|
||||
/** 滑窗大小(秒) */
|
||||
private static final long WINDOW_SECONDS = 5;
|
||||
|
||||
/** 限流冷却时间(秒) */
|
||||
private static final long COOLDOWN_WARN_SECONDS = 10;
|
||||
private static final long COOLDOWN_LIMIT_SECONDS = 30;
|
||||
private static final long COOLDOWN_BLOCK_SECONDS = 120;
|
||||
|
||||
/** 极端坐标阈值(距原点超30M格的区块) */
|
||||
private static final int EXTREME_COORD_THRESHOLD = 30_000_000;
|
||||
|
||||
/** 记录保留时间(秒) */
|
||||
private static final long RECORD_RETENTION_SECONDS = 300;
|
||||
|
||||
/** 区块加载响应阶段 */
|
||||
public enum Stage {
|
||||
NORMAL, // 正常
|
||||
WARN, // 警告
|
||||
LIMIT, // 限流
|
||||
BLOCK // 阻断
|
||||
}
|
||||
|
||||
public ChunkLoadRateLimiter() {
|
||||
this(new ServerGuardConfig());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public ChunkLoadRateLimiter(ServerGuardConfig config) {
|
||||
this.config = config;
|
||||
// 每 60 秒清理一次过期数据,防止内存泄漏
|
||||
scheduler.scheduleAtFixedRate(this::cleanupOldRecords, 60, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查一次区块加载请求是否应该被允许。
|
||||
*
|
||||
* 调用时机:Paper 的 ChunkLoadEvent 触发时,或代理拦截到区块数据包时。
|
||||
*
|
||||
* @param playerName 触发加载的玩家名称(可为 null 表示服务器内部加载)
|
||||
* @param playerUUID 玩家 UUID
|
||||
* @param worldName 世界名称
|
||||
* @param chunkX 区块 X 坐标
|
||||
* @param chunkZ 区块 Z 坐标
|
||||
* @param isServerInternal 是否为服务器内部触发的加载(非玩家移动所致)
|
||||
* @return 限流检查结果
|
||||
*/
|
||||
public ChunkLimitResult checkChunkLoad(String playerName, String playerUUID,
|
||||
String worldName, int chunkX, int chunkZ,
|
||||
boolean isServerInternal) {
|
||||
// 配置开关检查
|
||||
if (!config.getSecurity().getSuperEvolution().isChunkLoadRateLimiter()) {
|
||||
return ChunkLimitResult.clean();
|
||||
}
|
||||
|
||||
totalChunkLoads.incrementAndGet();
|
||||
|
||||
// 更新世界级别统计(所有加载都计数,包括服务器内部)
|
||||
worldChunkLoadTotal.computeIfAbsent(worldName, k -> new AtomicLong(0)).incrementAndGet();
|
||||
|
||||
// 服务器内部加载直接放行(世界生成、实体AI导致的被动加载)
|
||||
if (isServerInternal || playerName == null) {
|
||||
return ChunkLimitResult.clean();
|
||||
}
|
||||
|
||||
// 检查玩家是否处于冷却期
|
||||
Instant cooldownUntil = playerCooldownUntil.get(playerName);
|
||||
if (cooldownUntil != null) {
|
||||
if (Instant.now().isBefore(cooldownUntil)) {
|
||||
// 仍在冷却期,直接返回当前阶段的阻止状态
|
||||
Stage currentStage = playerStage.getOrDefault(playerName, Stage.NORMAL);
|
||||
if (currentStage == Stage.BLOCK) {
|
||||
blockedChunkLoads.incrementAndGet();
|
||||
return ChunkLimitResult.blocked(List.of(
|
||||
"COOLDOWN_BLOCK: player " + playerName + " is blocked until " + cooldownUntil));
|
||||
} else if (currentStage == Stage.LIMIT) {
|
||||
// 限流阶段:随机拒绝 50% 的请求
|
||||
if (Math.random() < 0.5) {
|
||||
blockedChunkLoads.incrementAndGet();
|
||||
return ChunkLimitResult.blocked(List.of(
|
||||
"COOLDOWN_LIMIT: throttled load for " + playerName));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 冷却已过期,清除记录
|
||||
playerCooldownUntil.remove(playerName);
|
||||
playerStage.remove(playerName);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录本次区块加载
|
||||
Instant now = Instant.now();
|
||||
ChunkLoadEvent event = new ChunkLoadEvent(now, chunkX, chunkZ, worldName);
|
||||
List<ChunkLoadEvent> events = playerLoadHistory.computeIfAbsent(playerName,
|
||||
k -> Collections.synchronizedList(new ArrayList<>()));
|
||||
events.add(event);
|
||||
|
||||
// === 检测1:极端坐标 Chunk Ban 攻击检测 ===
|
||||
// 区块坐标超过 30M 格(1875000 个区块)为极端坐标
|
||||
// 攻击者通过 TP 或飞行到达这些位置使服务器尝试生成不可能的地形
|
||||
if (Math.abs(chunkX) > EXTREME_COORD_THRESHOLD || Math.abs(chunkZ) > EXTREME_COORD_THRESHOLD) {
|
||||
ExtremeCoordEvent extremeEvent = new ExtremeCoordEvent(now, playerName, chunkX, chunkZ, worldName);
|
||||
extremeCoordEvents.computeIfAbsent(playerName, k -> new ArrayList<>()).add(extremeEvent);
|
||||
chunkBanDetections.incrementAndGet();
|
||||
|
||||
// 极端坐标无需等待速率累积,直接 BLOCK
|
||||
playerStage.put(playerName, Stage.BLOCK);
|
||||
playerCooldownUntil.put(playerName, Instant.now().plusSeconds(COOLDOWN_BLOCK_SECONDS));
|
||||
blockedChunkLoads.incrementAndGet();
|
||||
return ChunkLimitResult.flagged(List.of(
|
||||
"CHUNK_BAN_ATTACK: extreme coordinate chunk load at (" + chunkX + ", " + chunkZ
|
||||
+ "), distance from origin exceeds " + EXTREME_COORD_THRESHOLD + " blocks"));
|
||||
}
|
||||
|
||||
// === 检测2:区块加载速率检测(5 秒滑动窗口) ===
|
||||
long windowStart = now.toEpochMilli() - (WINDOW_SECONDS * 1000);
|
||||
long chunksInWindow = events.stream()
|
||||
.filter(e -> e.timestamp.toEpochMilli() > windowStart)
|
||||
.count();
|
||||
|
||||
// 计算每秒速率
|
||||
double ratePerSecond = (double) chunksInWindow / WINDOW_SECONDS;
|
||||
List<String> reasons = new ArrayList<>();
|
||||
Stage newStage = Stage.NORMAL;
|
||||
|
||||
if (ratePerSecond > BLOCK_THRESHOLD) {
|
||||
// 严重超限:直接阻断
|
||||
reasons.add("CRITICAL_RATE: " + String.format("%.1f", ratePerSecond)
|
||||
+ " chunks/sec (block threshold: " + BLOCK_THRESHOLD + ")");
|
||||
newStage = Stage.BLOCK;
|
||||
} else if (ratePerSecond > LIMIT_THRESHOLD) {
|
||||
// 中度超限:限制加载
|
||||
reasons.add("HIGH_RATE: " + String.format("%.1f", ratePerSecond)
|
||||
+ " chunks/sec (limit threshold: " + LIMIT_THRESHOLD + ")");
|
||||
newStage = Stage.LIMIT;
|
||||
} else if (ratePerSecond > WARN_THRESHOLD) {
|
||||
// 轻度超限:警告记录
|
||||
reasons.add("ELEVATED_RATE: " + String.format("%.1f", ratePerSecond)
|
||||
+ " chunks/sec (warn threshold: " + WARN_THRESHOLD + ")");
|
||||
newStage = Stage.WARN;
|
||||
}
|
||||
|
||||
// === 检测3:方向一致性检测(区分正常探索和自动化扫描) ===
|
||||
// 正常玩家移动有方向惯性,Bot 扫描呈网格状来回跳跃
|
||||
if (events.size() >= 10) {
|
||||
double directionChanges = countDirectionChanges(events);
|
||||
double dirChangeRatio = directionChanges / Math.min(events.size(), 10);
|
||||
// 如果方向变化率 > 80%,说明是扫描式加载而非线性移动
|
||||
if (dirChangeRatio > 0.8 && ratePerSecond > WARN_THRESHOLD) {
|
||||
reasons.add("SCAN_PATTERN: " + String.format("%.0f%%", dirChangeRatio * 100)
|
||||
+ " direction changes, bot-like scanning detected");
|
||||
// 扫描模式升级响应等级
|
||||
if (newStage == Stage.WARN) newStage = Stage.LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用响应阶段
|
||||
if (newStage != Stage.NORMAL) {
|
||||
playerStage.put(playerName, newStage);
|
||||
long cooldownSeconds = switch (newStage) {
|
||||
case WARN -> COOLDOWN_WARN_SECONDS;
|
||||
case LIMIT -> COOLDOWN_LIMIT_SECONDS;
|
||||
case BLOCK -> COOLDOWN_BLOCK_SECONDS;
|
||||
default -> 0;
|
||||
};
|
||||
playerCooldownUntil.put(playerName, Instant.now().plusSeconds(cooldownSeconds));
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
if (newStage == Stage.BLOCK) {
|
||||
blockedChunkLoads.incrementAndGet();
|
||||
return ChunkLimitResult.flagged(reasons);
|
||||
} else if (newStage == Stage.LIMIT) {
|
||||
blockedChunkLoads.incrementAndGet();
|
||||
return ChunkLimitResult.blocked(reasons);
|
||||
} else if (newStage == Stage.WARN) {
|
||||
return ChunkLimitResult.suspicious((int) ratePerSecond, reasons);
|
||||
}
|
||||
|
||||
return ChunkLimitResult.clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最近区块加载事件的方向变化次数。
|
||||
* 相邻两次加载的区块坐标变化方向不同计为一次方向变化。
|
||||
*/
|
||||
private double countDirectionChanges(List<ChunkLoadEvent> events) {
|
||||
if (events.size() < 3) return 0;
|
||||
int changes = 0;
|
||||
int count = Math.min(events.size(), 12);
|
||||
int start = Math.max(0, events.size() - count);
|
||||
for (int i = start + 2; i < events.size(); i++) {
|
||||
int dx1 = events.get(i - 1).chunkX - events.get(i - 2).chunkX;
|
||||
int dz1 = events.get(i - 1).chunkZ - events.get(i - 2).chunkZ;
|
||||
int dx2 = events.get(i).chunkX - events.get(i - 1).chunkX;
|
||||
int dz2 = events.get(i).chunkZ - events.get(i - 1).chunkZ;
|
||||
// 方向变化:X 或 Z 方向的正负号发生变化
|
||||
if (Integer.signum(dx1) != Integer.signum(dx2)
|
||||
|| Integer.signum(dz1) != Integer.signum(dz2)) {
|
||||
changes++;
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定玩家的所有追踪数据(玩家退出时调用)。
|
||||
*/
|
||||
public void clearPlayer(String playerName) {
|
||||
playerLoadHistory.remove(playerName);
|
||||
playerStage.remove(playerName);
|
||||
playerCooldownUntil.remove(playerName);
|
||||
extremeCoordEvents.remove(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前模块运行状态与统计。
|
||||
*/
|
||||
public Map<String, Object> getStatus() {
|
||||
Map<String, Object> s = new LinkedHashMap<>();
|
||||
s.put("totalChunkLoads", totalChunkLoads.get());
|
||||
s.put("blockedChunkLoads", blockedChunkLoads.get());
|
||||
s.put("chunkBanDetections", chunkBanDetections.get());
|
||||
s.put("trackedPlayers", playerLoadHistory.size());
|
||||
s.put("activeStages", playerStage.size());
|
||||
s.put("enabled", config.getSecurity().getSuperEvolution().isChunkLoadRateLimiter());
|
||||
|
||||
// 世界级别统计
|
||||
Map<String, Object> worldStats = new LinkedHashMap<>();
|
||||
worldChunkLoadTotal.forEach((world, count) -> worldStats.put(world, count.get()));
|
||||
s.put("worldStats", worldStats);
|
||||
|
||||
// 当前受限玩家列表
|
||||
List<Map<String, Object>> limitedPlayers = new ArrayList<>();
|
||||
for (Map.Entry<String, Stage> e : playerStage.entrySet()) {
|
||||
if (e.getValue() != Stage.NORMAL) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("player", e.getKey());
|
||||
m.put("stage", e.getValue().name());
|
||||
Instant cooldown = playerCooldownUntil.get(e.getKey());
|
||||
m.put("cooldownUntil", cooldown != null ? cooldown.toString() : "N/A");
|
||||
List<ChunkLoadEvent> events = playerLoadHistory.get(e.getKey());
|
||||
if (events != null) {
|
||||
long recent = events.stream()
|
||||
.filter(ev -> ev.timestamp.isAfter(Instant.now().minusSeconds(WINDOW_SECONDS)))
|
||||
.count();
|
||||
m.put("recentLoads", recent);
|
||||
}
|
||||
limitedPlayers.add(m);
|
||||
}
|
||||
}
|
||||
s.put("limitedPlayers", limitedPlayers);
|
||||
|
||||
// 最近极端坐标事件
|
||||
List<Map<String, Object>> extremeEvents = new ArrayList<>();
|
||||
for (Map.Entry<String, List<ExtremeCoordEvent>> e : extremeCoordEvents.entrySet()) {
|
||||
for (ExtremeCoordEvent ev : e.getValue()) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("player", ev.playerName);
|
||||
m.put("chunkX", ev.chunkX);
|
||||
m.put("chunkZ", ev.chunkZ);
|
||||
m.put("world", ev.worldName);
|
||||
m.put("time", ev.timestamp.toString());
|
||||
extremeEvents.add(m);
|
||||
}
|
||||
}
|
||||
extremeEvents.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
|
||||
s.put("extremeCoordEvents", extremeEvents.subList(0, Math.min(extremeEvents.size(), 10)));
|
||||
return s;
|
||||
}
|
||||
|
||||
public long getTotalChunkLoads() { return totalChunkLoads.get(); }
|
||||
public long getBlockedChunkLoads() { return blockedChunkLoads.get(); }
|
||||
public long getChunkBanDetections() { return chunkBanDetections.get(); }
|
||||
|
||||
/**
|
||||
* 定期清理过期记录。
|
||||
*/
|
||||
private void cleanupOldRecords() {
|
||||
Instant cutoff = Instant.now().minusSeconds(RECORD_RETENTION_SECONDS);
|
||||
playerLoadHistory.entrySet().removeIf(e -> {
|
||||
List<ChunkLoadEvent> events = e.getValue();
|
||||
events.removeIf(ev -> ev.timestamp.isBefore(cutoff));
|
||||
return events.isEmpty();
|
||||
});
|
||||
extremeCoordEvents.entrySet().removeIf(e -> {
|
||||
List<ExtremeCoordEvent> events = e.getValue();
|
||||
events.removeIf(ev -> ev.timestamp.isBefore(cutoff));
|
||||
return events.isEmpty();
|
||||
});
|
||||
// 清理已过期的冷却
|
||||
playerCooldownUntil.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
|
||||
// 同步清理已过期冷却的阶段记录
|
||||
playerStage.keySet().removeIf(k -> !playerCooldownUntil.containsKey(k));
|
||||
}
|
||||
|
||||
// ==================== 内部数据类 ====================
|
||||
|
||||
/**
|
||||
* 区块加载事件记录。
|
||||
*/
|
||||
private static class ChunkLoadEvent {
|
||||
final Instant timestamp;
|
||||
final int chunkX;
|
||||
final int chunkZ;
|
||||
final String worldName;
|
||||
|
||||
ChunkLoadEvent(Instant timestamp, int chunkX, int chunkZ, String worldName) {
|
||||
this.timestamp = timestamp;
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
this.worldName = worldName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 极端坐标加载事件记录 — 用于 Chunk Ban 攻击溯源。
|
||||
*/
|
||||
private static class ExtremeCoordEvent {
|
||||
final Instant timestamp;
|
||||
final String playerName;
|
||||
final int chunkX;
|
||||
final int chunkZ;
|
||||
final String worldName;
|
||||
|
||||
ExtremeCoordEvent(Instant timestamp, String playerName, int chunkX, int chunkZ, String worldName) {
|
||||
this.timestamp = timestamp;
|
||||
this.playerName = playerName;
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
this.worldName = worldName;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 检测结果类 ====================
|
||||
|
||||
/**
|
||||
* 区块加载限流检测结果。
|
||||
*/
|
||||
public static class ChunkLimitResult {
|
||||
private final boolean blocked;
|
||||
private final boolean flagged;
|
||||
private final boolean suspicious;
|
||||
private final int score;
|
||||
private final List<String> reasons;
|
||||
|
||||
private ChunkLimitResult(boolean blocked, boolean flagged, boolean suspicious,
|
||||
int score, List<String> reasons) {
|
||||
this.blocked = blocked;
|
||||
this.flagged = flagged;
|
||||
this.suspicious = suspicious;
|
||||
this.score = score;
|
||||
this.reasons = reasons;
|
||||
}
|
||||
|
||||
/** 正常放行 */
|
||||
public static ChunkLimitResult clean() {
|
||||
return new ChunkLimitResult(false, false, false, 0, List.of());
|
||||
}
|
||||
|
||||
/** 可疑行为(警告阶段) */
|
||||
public static ChunkLimitResult suspicious(int score, List<String> reasons) {
|
||||
return new ChunkLimitResult(false, false, true, score, reasons);
|
||||
}
|
||||
|
||||
/** 阻止加载但不标记玩家 */
|
||||
public static ChunkLimitResult blocked(List<String> reasons) {
|
||||
return new ChunkLimitResult(true, false, true, 0, reasons);
|
||||
}
|
||||
|
||||
/** 已标记(严重违规,需要干预) */
|
||||
public static ChunkLimitResult flagged(List<String> reasons) {
|
||||
return new ChunkLimitResult(true, true, true, 100, reasons);
|
||||
}
|
||||
|
||||
public boolean isBlocked() { return blocked; }
|
||||
public boolean isFlagged() { return flagged; }
|
||||
public boolean isSuspicious() { return suspicious; }
|
||||
public boolean isClean() { return !blocked && !flagged && !suspicious; }
|
||||
public int getScore() { return score; }
|
||||
public List<String> getReasons() { return reasons; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user