超进化 v3.2: Minecraft专属安全模块 — X-ray透视/飞行外挂/物品复制/崩溃漏洞/卡服机检测

新增5个Minecraft反作弊安全模块 + 21项测试 + 5个API端点:
- AntiXrayDetectionService: 钻石/远古残骸挖掘比检测,直线挖掘模式识别
- AntiFlyDetectionService: 垂直/水平速度检测,飞行外挂判定
- AntiDupeDetectionService: 物品堆叠异常/高价值物品暴涨/9种复制方法检测
- CrashExploitProtectionService: 12种崩溃漏洞防护(超大包/NBT炸弹/书与笔攻击等)
- LagMachineDetectionService: Observer链/TNT堆/红石密度/实体泛滥检测
DashboardController新增 /api/security/anti-xray|anti-fly|anti-dupe|crash-exploit|lag-machine 端点
This commit is contained in:
Peijun Zhao
2026-05-10 21:11:00 +08:00
parent 939d257ce6
commit 1d297ba095
10 changed files with 1486 additions and 3 deletions
+6 -1
View File
@@ -810,7 +810,7 @@ serverguard
### 安全防护层 (`com.aluer.security`) ### 安全防护层 (`com.aluer.security`)
这是系统中规模最大的包,包含 60+ 个安全服务(含 20 个超进化 v3.1 新增模块): 这是系统中规模最大的包,包含 65+ 个安全服务(含 25 个超进化 v3.2 新增模块):
| 类别 | 类 | 功能 | | 类别 | 类 | 功能 |
|------|-----|------| |------|-----|------|
@@ -880,6 +880,11 @@ serverguard
| | `IncidentResponseService` | 5套响应剧本(DDoS/爆破/入侵/MC漏洞/数据泄露) | | | `IncidentResponseService` | 5套响应剧本(DDoS/爆破/入侵/MC漏洞/数据泄露) |
| | `ThreatHuntingService` | 10种主动狩猎规则(持久化/横向移动/提权/挖矿/WebShell) | | | `ThreatHuntingService` | 10种主动狩猎规则(持久化/横向移动/提权/挖矿/WebShell) |
| | `ComplianceScannerService` | 7大类20+项合规检查,自动评分(0-100%) | | | `ComplianceScannerService` | 7大类20+项合规检查,自动评分(0-100%) |
| **Minecraft专属** | `AntiXrayDetectionService` | X-ray透视检测:钻石/石头比率分析、直线挖掘、黑暗环境精准采矿 |
| | `AntiFlyDetectionService` | 飞行/速度/传送外挂检测:垂直限速、水平限速、留空时间、无摔伤检测 |
| | `AntiDupeDetectionService` | 物品复制检测:堆叠溢出、高价值物品暴涨、9种已知复制方法监测 |
| | `CrashExploitProtectionService` | 崩溃漏洞防护:超大包/负长度/NBT嵌套/书与笔/Sign炸弹/Tile Entity洪水 |
| | `LagMachineDetectionService` | 卡服机检测:Observer链/TNT堆叠/红石密度/实体刷怪/掉落方块洪流 |
### 运营控制层 (`com.aluer.console`) ### 运营控制层 (`com.aluer.console`)
+6 -1
View File
@@ -667,7 +667,7 @@ serverguard
### Security Defense Layer (`com.aluer.security`) ### Security Defense Layer (`com.aluer.security`)
The largest package in the system, containing 30+ security services: The largest package in the system, containing 65+ security services (with 25 Super Evolution v3.2 modules):
| Category | Key Classes | Description | | Category | Key Classes | Description |
|----------|------------|-------------| |----------|------------|-------------|
@@ -702,6 +702,11 @@ The largest package in the system, containing 30+ security services:
| | `IncidentResponseService` | 5 playbooks (DDoS/brute force/intrusion/MC exploit/data breach) | | | `IncidentResponseService` | 5 playbooks (DDoS/brute force/intrusion/MC exploit/data breach) |
| | `ThreatHuntingService` | 10 proactive hunt rules (persistence/lateral/privilege/crypto/webshell) | | | `ThreatHuntingService` | 10 proactive hunt rules (persistence/lateral/privilege/crypto/webshell) |
| | `ComplianceScannerService` | 7 categories 20+ checks, auto compliance scoring (0-100%) | | | `ComplianceScannerService` | 7 categories 20+ checks, auto compliance scoring (0-100%) |
| **Minecraft-Specific** | `AntiXrayDetectionService` | X-ray detection: diamond/stone ratio, straight-line mining, dark precision |
| | `AntiFlyDetectionService` | Fly/speed/teleport hack detection: vertical/horizontal limits, air time |
| | `AntiDupeDetectionService` | Item duplication detection: stack overflow, high-value item surges, 9 known methods |
| | `CrashExploitProtectionService` | Crash exploit protection: oversized packets, NBT bombs, book/sign exploits |
| | `LagMachineDetectionService` | Lag machine detection: observer chains, TNT stacks, entity spam, falling blocks |
### Operations Control Layer (`com.aluer.console`) ### Operations Control Layer (`com.aluer.console`)
+10
View File
@@ -442,6 +442,16 @@ export ALUER_WEBHOOK_SLACK="https://hooks.slack.com/services/xxx/yyy/zzz"
| `/api/security/threat-hunting/status` | GET | 威胁狩猎状态(最近狩猎结果) | | `/api/security/threat-hunting/status` | GET | 威胁狩猎状态(最近狩猎结果) |
| `/api/security/compliance/status` | GET | 合规扫描结果(评分、通过/失败项) | | `/api/security/compliance/status` | GET | 合规扫描结果(评分、通过/失败项) |
#### Minecraft 专属安全(v3.2 新增)
| 接口路径 | 方法 | 说明 |
|----------|------|------|
| `/api/security/anti-xray/status` | GET | X-ray透视检测状态(可疑玩家、钻石/石头比) |
| `/api/security/anti-fly/status` | GET | 飞行/速度外挂检测状态 |
| `/api/security/anti-dupe/status` | GET | 物品复制检测状态(堆叠异常、高价值物品暴涨) |
| `/api/security/crash-exploit/status` | GET | 崩溃漏洞防护状态(超大包、NBT炸弹、书与笔攻击) |
| `/api/security/lag-machine/status` | GET | 卡服机检测状态(Observer链、TNT堆、红石密度) |
--- ---
## 配置说明 ## 配置说明
@@ -0,0 +1,276 @@
package com.aluer.security;
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;
@Service
public class AntiDupeDetectionService {
private final Map<String, PlayerInventorySnapshot> inventorySnapshots = new ConcurrentHashMap<>();
private final Map<String, List<DupeEvent>> dupeEvents = new ConcurrentHashMap<>();
private final Map<String, Instant> flaggedPlayers = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final int MAX_ITEM_STACK_SIZE = 64;
private static final int MAX_SHULKER_ITEMS = 27;
private static final int MAX_DIAMOND_STACK = 64;
private static final int MAX_NETHERITE_STACK = 64;
private static final int MAX_TOTEM_STACK = 1;
private static final long FLAG_DURATION_SECONDS = 7200;
private static final Set<String> HIGH_VALUE_ITEMS = Set.of(
"DIAMOND", "DIAMOND_BLOCK", "NETHERITE_INGOT", "NETHERITE_BLOCK",
"NETHERITE_SCRAP", "ANCIENT_DEBRIS", "EMERALD", "EMERALD_BLOCK",
"TOTEM_OF_UNDYING", "ELYTRA", "BEACON", "NETHER_STAR",
"ENCHANTED_GOLDEN_APPLE", "DRAGON_EGG", "TRIDENT",
"SHULKER_SHELL", "ENDER_PEARL", "BLAZE_ROD", "GUNPOWDER"
);
private static final Set<DupeMethod> KNOWN_DUPE_METHODS = new HashSet<>();
static {
KNOWN_DUPE_METHODS.add(new DupeMethod("BOOK_DUPE", "Book and quill crafting dupe", 85));
KNOWN_DUPE_METHODS.add(new DupeMethod("DONKEY_DUPE", "Donkey/llama chest dupe", 90));
KNOWN_DUPE_METHODS.add(new DupeMethod("ENDER_CHEST_DUPE", "Ender chest rapid open/close dupe", 80));
KNOWN_DUPE_METHODS.add(new DupeMethod("PORTAL_DUPE", "Nether/End portal item dupe", 75));
KNOWN_DUPE_METHODS.add(new DupeMethod("SHULKER_DUPE", "Shulker box stacking dupe", 90));
KNOWN_DUPE_METHODS.add(new DupeMethod("DISPENSER_DUPE", "Dispenser/dropper dupe", 70));
KNOWN_DUPE_METHODS.add(new DupeMethod("CHEST_BOAT_DUPE", "Chest boat item transfer dupe", 75));
KNOWN_DUPE_METHODS.add(new DupeMethod("PISTON_DUPE", "Piston-based duplication", 80));
KNOWN_DUPE_METHODS.add(new DupeMethod("ITEM_FRAME_DUPE", "Item frame duplication glitch", 85));
}
public AntiDupeDetectionService() {
scheduler.scheduleAtFixedRate(this::cleanupOldData, 300, 600, TimeUnit.SECONDS);
}
public DupeCheckResult checkInventoryChange(String playerName, String itemType, int newCount, int oldCount,
String containerType, int x, int y, int z) {
if (flaggedPlayers.containsKey(playerName)) {
if (Instant.now().isAfter(flaggedPlayers.get(playerName))) {
flaggedPlayers.remove(playerName);
} else {
return DupeCheckResult.flagged("Player is flagged for duplication");
}
}
int score = 0;
List<String> reasons = new ArrayList<>();
// Rule 1: Stack size violation
int maxStack = getMaxStackSize(itemType);
if (newCount > maxStack && !isShulkerBox(containerType)) {
score += 40;
reasons.add("STACK_OVERFLOW: " + itemType + " count=" + newCount + " (max=" + maxStack + ")");
}
// Rule 2: Rapid high-value item gain
int gain = newCount - oldCount;
if (gain > 0 && HIGH_VALUE_ITEMS.contains(itemType.toUpperCase())) {
PlayerInventorySnapshot snap = inventorySnapshots.computeIfAbsent(playerName, k -> new PlayerInventorySnapshot());
snap.recordGain(itemType, gain);
if (snap.recentHighValueGain > 100) {
score += 35;
reasons.add("MASS_GAIN: " + snap.recentHighValueGain + " high-value items gained");
}
}
// Rule 3: Impossible inventory transition
if (newCount > oldCount * 3 && oldCount > 0 && newCount > 500) {
score += 30;
reasons.add("IMPOSSIBLE_INCREASE: " + itemType + " " + oldCount + " -> " + newCount);
}
// Rule 4: Container interaction frequency (dupe method indicator)
PlayerInventorySnapshot snap = inventorySnapshots.computeIfAbsent(playerName, k -> new PlayerInventorySnapshot());
snap.recordContainerOpen(containerType);
if (snap.containerOpensPerMinute > 30) {
score += 20;
reasons.add("CONTAINER_SPAM: " + snap.containerOpensPerMinute + "/min");
}
// Rule 5: Netherite/Ancient Debris gain without mining (possible creative/give bypass)
if (("NETHERITE_INGOT".equalsIgnoreCase(itemType) || "NETHERITE_BLOCK".equalsIgnoreCase(itemType)) && gain >= 64) {
score += 50;
reasons.add("NETHERITE_SURGE: +" + gain + " " + itemType);
}
if (score >= 40) {
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
DupeEvent event = new DupeEvent(Instant.now(), playerName, itemType, oldCount, newCount, containerType, reasons);
dupeEvents.computeIfAbsent(playerName, k -> new ArrayList<>()).add(event);
totalDetections.incrementAndGet();
return DupeCheckResult.flagged("Duplication detected: " + String.join("; ", reasons));
} else if (score >= 25) {
return DupeCheckResult.suspicious(score, reasons);
}
return DupeCheckResult.clean();
}
public DupeCheckResult checkItemDropPickup(String playerName, String itemType, int itemCount, double pickupTimeSeconds) {
int score = 0;
List<String> reasons = new ArrayList<>();
// Extremely rapid pickup cycle (classic dupe pattern)
if (pickupTimeSeconds < 0.05 && itemCount > 1000) {
score += 40;
reasons.add("RAPID_PICKUP_CYCLE: " + itemCount + " " + itemType + " in " + pickupTimeSeconds + "s");
}
if (itemCount > 10000) {
score += 50;
reasons.add("MASS_DUMP: " + itemCount + " " + itemType);
}
if (score >= 40) {
totalDetections.incrementAndGet();
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
return DupeCheckResult.flagged("Suspicious item pickup: " + String.join("; ", reasons));
}
return DupeCheckResult.clean();
}
public void clearPlayer(String playerName) {
inventorySnapshots.remove(playerName);
flaggedPlayers.remove(playerName);
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedPlayers", inventorySnapshots.size());
status.put("flaggedPlayers", flaggedPlayers.size());
status.put("totalDetections", totalDetections.get());
status.put("knownDupeMethods", KNOWN_DUPE_METHODS.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<DupeEvent>> e : dupeEvents.entrySet()) {
for (DupeEvent event : e.getValue()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("item", event.itemType);
m.put("oldCount", event.oldCount);
m.put("newCount", event.newCount);
m.put("container", event.containerType);
m.put("reasons", event.reasons);
m.put("time", event.timestamp.toString());
recent.add(m);
}
}
recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20)));
return status;
}
public long getTotalDetections() { return totalDetections.get(); }
private int getMaxStackSize(String itemType) {
if (itemType == null) return MAX_ITEM_STACK_SIZE;
String upper = itemType.toUpperCase();
if (upper.contains("TOTEM")) return MAX_TOTEM_STACK;
if (upper.contains("PICKAXE") || upper.contains("SWORD") || upper.contains("AXE")
|| upper.contains("SHOVEL") || upper.contains("HOE")) return 1;
if (upper.contains("BOOTS") || upper.contains("CHESTPLATE") || upper.contains("HELMET")
|| upper.contains("LEGGINGS") || upper.contains("ELYTRA")) return 1;
if (upper.contains("BUCKET") || upper.contains("POTION") || upper.contains("BED")) return 1;
return MAX_ITEM_STACK_SIZE;
}
private boolean isShulkerBox(String containerType) {
return containerType != null && containerType.toUpperCase().contains("SHULKER");
}
private void cleanupOldData() {
Instant cutoff = Instant.now().minusSeconds(1200);
inventorySnapshots.entrySet().removeIf(e -> e.getValue().lastActivity.isBefore(cutoff));
flaggedPlayers.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
}
private static class PlayerInventorySnapshot {
int containerOpensPerMinute;
int recentHighValueGain;
Instant lastActivity = Instant.now();
Instant minuteStart = Instant.now();
void recordGain(String item, int gain) {
lastActivity = Instant.now();
if (HIGH_VALUE_ITEMS.contains(item.toUpperCase())) {
recentHighValueGain += gain;
}
if (Instant.now().isAfter(minuteStart.plusSeconds(60))) {
containerOpensPerMinute = 0;
recentHighValueGain = 0;
minuteStart = Instant.now();
}
}
void recordContainerOpen(String container) {
containerOpensPerMinute++;
lastActivity = Instant.now();
}
}
private static class DupeEvent {
final Instant timestamp;
final String playerName;
final String itemType;
final int oldCount;
final int newCount;
final String containerType;
final List<String> reasons;
DupeEvent(Instant timestamp, String playerName, String itemType, int oldCount, int newCount,
String containerType, List<String> reasons) {
this.timestamp = timestamp;
this.playerName = playerName;
this.itemType = itemType;
this.oldCount = oldCount;
this.newCount = newCount;
this.containerType = containerType;
this.reasons = reasons;
}
}
private static class DupeMethod {
final String name;
final String description;
final int severity;
DupeMethod(String name, String description, int severity) {
this.name = name;
this.description = description;
this.severity = severity;
}
}
public static class DupeCheckResult {
private final boolean flagged;
private final boolean suspicious;
private final int score;
private final List<String> reasons;
private DupeCheckResult(boolean flagged, boolean suspicious, int score, List<String> reasons) {
this.flagged = flagged;
this.suspicious = suspicious;
this.score = score;
this.reasons = reasons;
}
public static DupeCheckResult clean() { return new DupeCheckResult(false, false, 0, List.of()); }
public static DupeCheckResult suspicious(int score, List<String> reasons) { return new DupeCheckResult(false, true, score, reasons); }
public static DupeCheckResult flagged(String msg) { return new DupeCheckResult(true, true, 100, List.of(msg)); }
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}
@@ -0,0 +1,198 @@
package com.aluer.security;
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;
@Service
public class AntiFlyDetectionService {
private final Map<String, PlayerMovementData> movementData = new ConcurrentHashMap<>();
private final Map<String, Instant> flaggedPlayers = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final double MAX_VERTICAL_SPEED = 0.42;
private static final double MAX_HORIZONTAL_SPEED_SURVIVAL = 9.0;
private static final double MAX_HORIZONTAL_SPEED_CREATIVE = 15.0;
private static final double MAX_VERTICAL_SPEED_ELYTRA = 3.0;
private static final int MAX_CONSECUTIVE_AIR_TICKS = 20;
private static final double IMPOSSIBLE_Y_CHANGE = 5.0;
private static final int MOVEMENT_BUFFER_SIZE = 100;
private static final int MIN_MOVEMENTS_FOR_ANALYSIS = 20;
private static final long FLAG_DURATION_SECONDS = 3600;
public AntiFlyDetectionService() {
scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS);
}
public FlyCheckResult checkMovement(String playerName, double fromX, double fromY, double fromZ,
double toX, double toY, double toZ, boolean onGround, String gameMode) {
if (flaggedPlayers.containsKey(playerName)) {
if (Instant.now().isAfter(flaggedPlayers.get(playerName))) {
flaggedPlayers.remove(playerName);
} else {
return FlyCheckResult.flagged("Player is flagged for movement hacks");
}
}
PlayerMovementData data = movementData.computeIfAbsent(playerName, k -> new PlayerMovementData());
data.recordMovement(fromX, fromY, fromZ, toX, toY, toZ, onGround);
int score = 0;
List<String> reasons = new ArrayList<>();
double dx = toX - fromX;
double dy = toY - fromY;
double dz = toZ - fromZ;
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
double verticalSpeed = Math.abs(dy);
// Rule 1: Impossible vertical movement (no fall + rapid rise, not elytra)
boolean isCreative = "creative".equalsIgnoreCase(gameMode) || "spectator".equalsIgnoreCase(gameMode);
if (!onGround && !isCreative && verticalSpeed > MAX_VERTICAL_SPEED * 3 && dy > 3.0) {
score += 35;
reasons.add("IMPOSSIBLE_VERTICAL: " + String.format("%.2f", verticalSpeed) + " blocks/tick (no ground)");
}
// Rule 2: Excessive horizontal speed
double gameModeLimit = "creative".equalsIgnoreCase(gameMode) || "spectator".equalsIgnoreCase(gameMode)
? MAX_HORIZONTAL_SPEED_CREATIVE : MAX_HORIZONTAL_SPEED_SURVIVAL;
if (horizontalDist > gameModeLimit) {
score += 35;
reasons.add("EXCESSIVE_HORIZONTAL: " + String.format("%.2f", horizontalDist) + " blocks/tick");
}
// Rule 3: Sustained air time (flying without elytra)
if (!onGround) {
data.consecutiveAirTicks++;
if (data.consecutiveAirTicks > MAX_CONSECUTIVE_AIR_TICKS && verticalSpeed < 0.1 && verticalSpeed > -0.5) {
score += 25;
reasons.add("SUSTAINED_AIR: " + data.consecutiveAirTicks + " ticks");
}
} else {
data.consecutiveAirTicks = 0;
}
// Rule 4: No fall damage (anti-knockback/anti-fall)
if (!onGround && dy < -IMPOSSIBLE_Y_CHANGE && !data.prevOnGround) {
data.freefallDistance += Math.abs(dy);
}
if (onGround && data.freefallDistance > 10) {
score += 20;
reasons.add("NO_FALL_DAMAGE: fell " + String.format("%.1f", data.freefallDistance) + " blocks");
data.freefallDistance = 0;
}
// Rule 5: Phase detection (moving through blocks)
if (data.totalMovements > MIN_MOVEMENTS_FOR_ANALYSIS) {
double avgSpeed = data.totalDistance / Math.max(1, data.totalMovements);
if (avgSpeed > MAX_HORIZONTAL_SPEED_SURVIVAL * 1.5 && !"creative".equalsIgnoreCase(gameMode)) {
score += 15;
reasons.add("HIGH_AVERAGE_SPEED: " + String.format("%.2f", avgSpeed) + " avg");
}
}
// Rule 6: Teleport detection
if (horizontalDist > 50 && "survival".equalsIgnoreCase(gameMode)) {
score += 40;
reasons.add("TELEPORT_DETECTED: " + String.format("%.1f", horizontalDist) + " blocks");
}
if (score >= 35) {
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
totalDetections.incrementAndGet();
return FlyCheckResult.flagged("Movement hack detected: " + String.join("; ", reasons));
} else if (score >= 30) {
return FlyCheckResult.suspicious(score, reasons);
}
return FlyCheckResult.clean();
}
public void clearPlayer(String playerName) {
movementData.remove(playerName);
flaggedPlayers.remove(playerName);
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedPlayers", movementData.size());
status.put("flaggedPlayers", flaggedPlayers.size());
status.put("totalDetections", totalDetections.get());
List<Map<String, Object>> topSuspicious = new ArrayList<>();
for (Map.Entry<String, PlayerMovementData> e : movementData.entrySet()) {
PlayerMovementData d = e.getValue();
double avgSpeed = d.totalMovements > 0 ? d.totalDistance / d.totalMovements : 0;
if (avgSpeed > 1.0 || d.consecutiveAirTicks > 10) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("avgSpeed", String.format("%.2f", avgSpeed));
m.put("airTicks", d.consecutiveAirTicks);
m.put("totalMovements", d.totalMovements);
topSuspicious.add(m);
}
}
topSuspicious.sort((a, b) -> {
double sa = Double.parseDouble(a.get("avgSpeed").toString());
double sb = Double.parseDouble(b.get("avgSpeed").toString());
return Double.compare(sb, sa);
});
status.put("suspiciousPlayers", topSuspicious.subList(0, Math.min(topSuspicious.size(), 20)));
return status;
}
public long getTotalDetections() { return totalDetections.get(); }
private void cleanupOldData() {
Instant cutoff = Instant.now().minusSeconds(600);
movementData.entrySet().removeIf(e -> e.getValue().lastActivity.isBefore(cutoff));
flaggedPlayers.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
}
private static class PlayerMovementData {
int totalMovements;
double totalDistance;
int consecutiveAirTicks;
double freefallDistance;
boolean prevOnGround;
Instant lastActivity = Instant.now();
void recordMovement(double fx, double fy, double fz, double tx, double ty, double tz, boolean onGround) {
totalMovements++;
totalDistance += Math.sqrt(Math.pow(tx - fx, 2) + Math.pow(ty - fy, 2) + Math.pow(tz - fz, 2));
lastActivity = Instant.now();
prevOnGround = onGround;
}
}
public static class FlyCheckResult {
private final boolean flagged;
private final boolean suspicious;
private final int score;
private final List<String> reasons;
private FlyCheckResult(boolean flagged, boolean suspicious, int score, List<String> reasons) {
this.flagged = flagged;
this.suspicious = suspicious;
this.score = score;
this.reasons = reasons;
}
public static FlyCheckResult clean() { return new FlyCheckResult(false, false, 0, List.of()); }
public static FlyCheckResult suspicious(int score, List<String> reasons) { return new FlyCheckResult(false, true, score, reasons); }
public static FlyCheckResult flagged(String msg) { return new FlyCheckResult(true, true, 100, List.of(msg)); }
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}
@@ -0,0 +1,250 @@
package com.aluer.security;
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;
@Service
public class AntiXrayDetectionService {
private final Map<String, PlayerMiningData> miningData = new ConcurrentHashMap<>();
private final Map<String, Instant> flaggedPlayers = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final double DIAMOND_STONE_RATIO_THRESHOLD = 0.5;
private static final double EMERALD_ORE_RATIO_THRESHOLD = 0.3;
private static final int MIN_BLOCKS_FOR_ANALYSIS = 30;
private static final int MAX_VALUABLE_STRAIGHT_LINE = 8;
private static final long MINING_SESSION_WINDOW_MS = 300000;
private static final long FLAG_DURATION_SECONDS = 3600;
private static final Set<String> VALUABLE_ORES = Set.of(
"DIAMOND_ORE", "DEEPSLATE_DIAMOND_ORE", "EMERALD_ORE", "DEEPSLATE_EMERALD_ORE",
"ANCIENT_DEBRIS", "NETHER_GOLD_ORE", "GILDED_BLACKSTONE",
"GOLD_ORE", "DEEPSLATE_GOLD_ORE"
);
private static final Set<String> COMMON_BLOCKS = Set.of(
"STONE", "DEEPSLATE", "ANDESITE", "DIORITE", "GRANITE",
"NETHERRACK", "BLACKSTONE", "BASALT", "TUFF", "CALCITE",
"DIRT", "GRAVEL", "SAND", "SANDSTONE"
);
private static final Set<String> CONTAINER_BLOCKS = Set.of(
"CHEST", "TRAPPED_CHEST", "ENDER_CHEST", "BARREL", "SHULKER_BOX",
"WHITE_SHULKER_BOX", "ORANGE_SHULKER_BOX", "MAGENTA_SHULKER_BOX",
"LIGHT_BLUE_SHULKER_BOX", "YELLOW_SHULKER_BOX", "LIME_SHULKER_BOX",
"PINK_SHULKER_BOX", "GRAY_SHULKER_BOX", "LIGHT_GRAY_SHULKER_BOX",
"CYAN_SHULKER_BOX", "PURPLE_SHULKER_BOX", "BLUE_SHULKER_BOX",
"BROWN_SHULKER_BOX", "GREEN_SHULKER_BOX", "RED_SHULKER_BOX", "BLACK_SHULKER_BOX"
);
public AntiXrayDetectionService() {
scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS);
}
public XrayCheckResult checkBlockBreak(String playerName, String blockType, int x, int y, int z, String world) {
if (flaggedPlayers.containsKey(playerName)) {
if (Instant.now().isAfter(flaggedPlayers.get(playerName))) {
flaggedPlayers.remove(playerName);
} else {
return XrayCheckResult.flagged("Player is flagged for X-ray behavior");
}
}
PlayerMiningData data = miningData.computeIfAbsent(playerName, k -> new PlayerMiningData());
data.recordBreak(blockType, x, y, z);
int score = 0;
List<String> reasons = new ArrayList<>();
// Rule 1: Diamond-to-stone ratio anomaly
int valuableCount = data.valuableOresBroken;
int commonCount = data.commonBlocksBroken;
if (commonCount >= MIN_BLOCKS_FOR_ANALYSIS) {
double ratio = (double) valuableCount / commonCount;
if (ratio > DIAMOND_STONE_RATIO_THRESHOLD && valuableCount >= 5) {
score += 30;
reasons.add("HIGH_VALUABLE_RATIO: " + String.format("%.2f", ratio) + " (v:" + valuableCount + "/c:" + commonCount + ")");
}
}
// Rule 2: Straight-line valuable mining (classic X-ray pattern)
if (VALUABLE_ORES.contains(blockType.toUpperCase())) {
data.lastValuableX = x;
data.lastValuableY = y;
data.lastValuableZ = z;
data.consecutiveValuable++;
if (data.consecutiveValuable > MAX_VALUABLE_STRAIGHT_LINE) {
score += 30;
reasons.add("STRAIGHT_LINE_MINING: " + data.consecutiveValuable + " consecutive valuable blocks");
}
} else {
data.consecutiveValuable = 0;
}
// Rule 3: Mining directly to ores without exploring
if (VALUABLE_ORES.contains(blockType.toUpperCase())) {
// Check if there's a suspicious pattern of going directly to ores
if (data.totalBlocksBroken > 20 && data.unusualPathCount > 5) {
score += 20;
reasons.add("DIRECT_ORE_PATHING: " + data.unusualPathCount + " unusual paths");
}
}
// Rule 4: Mining containers behind walls
if (CONTAINER_BLOCKS.contains(blockType.toUpperCase())) {
if (data.isMiningThroughWalls()) {
score += 15;
reasons.add("WALL_CONTAINER_ACCESS: " + blockType);
}
}
// Rule 5: Low light level mining precision
if (VALUABLE_ORES.contains(blockType.toUpperCase()) && isMiningInDarkness(world, y)) {
score += 10;
reasons.add("DARK_MINING_PRECISION: y=" + y + " in " + world);
}
if (score >= 60) {
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
totalDetections.incrementAndGet();
return XrayCheckResult.flagged("X-ray detected: " + String.join("; ", reasons));
} else if (score >= 30) {
return XrayCheckResult.suspicious(score, reasons);
}
return XrayCheckResult.clean();
}
public void clearPlayer(String playerName) {
miningData.remove(playerName);
flaggedPlayers.remove(playerName);
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedPlayers", miningData.size());
status.put("flaggedPlayers", flaggedPlayers.size());
status.put("totalDetections", totalDetections.get());
List<Map<String, Object>> topMiners = new ArrayList<>();
for (Map.Entry<String, PlayerMiningData> e : miningData.entrySet()) {
PlayerMiningData d = e.getValue();
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("valuableOres", d.valuableOresBroken);
m.put("commonBlocks", d.commonBlocksBroken);
double ratio = d.commonBlocksBroken > 0 ? (double) d.valuableOresBroken / d.commonBlocksBroken : 0;
m.put("ratio", String.format("%.3f", ratio));
m.put("consecutiveValuable", d.consecutiveValuable);
topMiners.add(m);
}
topMiners.sort((a, b) -> Double.compare(
Double.parseDouble(b.get("ratio").toString()),
Double.parseDouble(a.get("ratio").toString())));
status.put("topMiners", topMiners.subList(0, Math.min(topMiners.size(), 20)));
return status;
}
public long getTotalDetections() { return totalDetections.get(); }
private boolean isMiningInDarkness(String world, int y) {
return (world.contains("_nether") || y < 10);
}
private void cleanupOldData() {
Instant cutoff = Instant.now().minusSeconds(MINING_SESSION_WINDOW_MS * 2);
miningData.entrySet().removeIf(e -> e.getValue().lastActivity.isBefore(cutoff));
flaggedPlayers.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
}
private static class PlayerMiningData {
int totalBlocksBroken;
int valuableOresBroken;
int commonBlocksBroken;
int consecutiveValuable;
int unusualPathCount;
int lastValuableX, lastValuableY, lastValuableZ;
Instant lastActivity = Instant.now();
final List<BlockRecord> blockHistory = new ArrayList<>();
void recordBreak(String blockType, int x, int y, int z) {
totalBlocksBroken++;
lastActivity = Instant.now();
if (VALUABLE_ORES.contains(blockType.toUpperCase())) {
valuableOresBroken++;
if (lastValuableX != 0 || lastValuableY != 0 || lastValuableZ != 0) {
double dist = Math.sqrt(
Math.pow(x - lastValuableX, 2) +
Math.pow(y - lastValuableY, 2) +
Math.pow(z - lastValuableZ, 2));
if (dist > 20 && dist < 100) unusualPathCount++;
}
}
if (COMMON_BLOCKS.contains(blockType.toUpperCase())) {
commonBlocksBroken++;
}
blockHistory.add(new BlockRecord(blockType, x, y, z, Instant.now()));
if (blockHistory.size() > 200) {
blockHistory.remove(0);
}
consecutiveValuable = VALUABLE_ORES.contains(blockType.toUpperCase()) ? consecutiveValuable + 1 : 0;
}
boolean isMiningThroughWalls() {
if (blockHistory.size() < 10) return false;
int throughWallCount = 0;
for (int i = 3; i < blockHistory.size(); i++) {
if (CONTAINER_BLOCKS.contains(blockHistory.get(i).type.toUpperCase())) {
throughWallCount++;
}
}
return throughWallCount >= 3;
}
}
private static class BlockRecord {
final String type;
final int x, y, z;
final Instant time;
BlockRecord(String type, int x, int y, int z, Instant time) {
this.type = type;
this.x = x; this.y = y; this.z = z;
this.time = time;
}
}
public static class XrayCheckResult {
private final boolean flagged;
private final boolean suspicious;
private final int score;
private final List<String> reasons;
private XrayCheckResult(boolean flagged, boolean suspicious, int score, List<String> reasons) {
this.flagged = flagged;
this.suspicious = suspicious;
this.score = score;
this.reasons = reasons;
}
public static XrayCheckResult clean() { return new XrayCheckResult(false, false, 0, List.of()); }
public static XrayCheckResult suspicious(int score, List<String> reasons) { return new XrayCheckResult(false, true, score, reasons); }
public static XrayCheckResult flagged(String msg) { return new XrayCheckResult(true, true, 100, List.of(msg)); }
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}
@@ -0,0 +1,237 @@
package com.aluer.security;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class CrashExploitProtectionService {
private final Map<String, List<CrashAttempt>> crashAttempts = new ConcurrentHashMap<>();
private final Map<String, Instant> blockedSources = new ConcurrentHashMap<>();
private final AtomicLong totalDetections = new AtomicLong(0);
private final AtomicLong totalBlocks = new AtomicLong(0);
private static final long BLOCK_DURATION_SECONDS = 3600;
// Known Minecraft crash exploit patterns
private static final Map<String, CrashExploit> CRASH_EXPLOITS = new LinkedHashMap<>();
static {
CRASH_EXPLOITS.put("OVERSIZED_PACKET", new CrashExploit(
"Oversized packet attack", "Packet exceeding maximum size", 90, true));
CRASH_EXPLOITS.put("NEGATIVE_LENGTH", new CrashExploit(
"Negative packet length", "Packet with negative length value", 95, true));
CRASH_EXPLOITS.put("CHUNK_FLOOD", new CrashExploit(
"Chunk loading flood", "Rapid chunk loading requests to exhaust memory", 80, true));
CRASH_EXPLOITS.put("NBT_BOMB", new CrashExploit(
"NBT bomb", "Deeply nested NBT data causing stack overflow", 95, true));
CRASH_EXPLOITS.put("BOOK_CRASH", new CrashExploit(
"Book and quill crash", "Malformed book JSON causing parser crash", 90, true));
CRASH_EXPLOITS.put("SIGN_BOMB", new CrashExploit(
"Sign JSON bomb", "Oversized sign JSON text (8KB+ per line)", 90, true));
CRASH_EXPLOITS.put("ANVIL_CRASH", new CrashExploit(
"Anvil rename crash", "Extremely long anvil name causing crash", 75, true));
CRASH_EXPLOITS.put("TILE_ENTITY_FLOOD", new CrashExploit(
"Tile entity flood", "Mass tile entity creation in one chunk", 70, false));
CRASH_EXPLOITS.put("COMMAND_BLOCK_LOOP", new CrashExploit(
"Command block loop", "Self-referencing command block recursion", 85, true));
CRASH_EXPLOITS.put("RESOURCE_PACK_CRASH", new CrashExploit(
"Resource pack crash", "Extremely large resource pack causing OOM", 70, false));
CRASH_EXPLOITS.put("LIGHT_UPDATE_FLOOD", new CrashExploit(
"Light update flood", "Massive light updates causing TPS crash", 65, false));
CRASH_EXPLOITS.put("PARTICLE_CRASH", new CrashExploit(
"Particle crash", "Particle flood causing GPU/CPU exhaustion", 75, false));
}
public CrashCheckResult checkPacket(String packetType, int packetSize, String sourceIP) {
if (blockedSources.containsKey(sourceIP)) {
if (Instant.now().isAfter(blockedSources.get(sourceIP))) {
blockedSources.remove(sourceIP);
} else {
return CrashCheckResult.blocked("Source blocked for crash exploit attempts");
}
}
int score = 0;
List<String> reasons = new ArrayList<>();
// Check oversized packets
if (packetSize > 2097152) { // 2MB in bytes
score += 50;
reasons.add("OVERSIZED_PACKET: " + packetSize + " bytes");
}
if (packetSize < 0) {
score += 90;
reasons.add("NEGATIVE_LENGTH: " + packetSize);
}
if (packetSize > 65535 && packetType.contains("CustomPayload")) {
score += 40;
reasons.add("LARGE_CUSTOM_PAYLOAD: " + packetSize);
}
// Check for known crash exploit types
for (Map.Entry<String, CrashExploit> e : CRASH_EXPLOITS.entrySet()) {
if (packetType.toUpperCase().contains(e.getKey())) {
score += e.getValue().severity;
reasons.add("CRASH_EXPLOIT: " + e.getValue().description);
}
}
if (score >= 70) {
blockedSources.put(sourceIP, Instant.now().plusSeconds(BLOCK_DURATION_SECONDS));
CrashAttempt attempt = new CrashAttempt(Instant.now(), sourceIP, packetType, packetSize, reasons);
crashAttempts.computeIfAbsent(sourceIP, k -> new ArrayList<>()).add(attempt);
totalDetections.incrementAndGet();
totalBlocks.incrementAndGet();
return CrashCheckResult.detected(reasons, score);
} else if (score >= 30) {
return CrashCheckResult.suspicious(reasons, score);
}
return CrashCheckResult.clean();
}
public CrashCheckResult checkNBTData(String nbtTag, int nestDepth, String sourceIP) {
int score = 0;
List<String> reasons = new ArrayList<>();
if (nestDepth > 64) {
score += 90;
reasons.add("NBT_DEEP_NESTING: depth=" + nestDepth);
}
if (nestDepth > 32) {
score += 40;
reasons.add("NBT_MODERATE_NESTING: depth=" + nestDepth);
}
if (nbtTag != null && nbtTag.length() > 65536) {
score += 60;
reasons.add("NBT_OVERSIZED: " + nbtTag.length() + " chars");
}
if (score >= 70) {
blockedSources.put(sourceIP, Instant.now().plusSeconds(BLOCK_DURATION_SECONDS));
totalDetections.incrementAndGet();
totalBlocks.incrementAndGet();
return CrashCheckResult.detected(reasons, score);
}
return score > 0 ? CrashCheckResult.suspicious(reasons, score) : CrashCheckResult.clean();
}
public CrashCheckResult checkBookContent(String bookJSON, int pageCount, int totalChars, String sourceIP) {
int score = 0;
List<String> reasons = new ArrayList<>();
if (totalChars > 32768) {
score += 70;
reasons.add("BOOK_OVERSIZED: " + totalChars + " total chars across " + pageCount + " pages");
}
if (pageCount > 100) {
score += 50;
reasons.add("BOOK_EXCESSIVE_PAGES: " + pageCount);
}
if (bookJSON != null && bookJSON.contains("\"clickEvent\"") && bookJSON.contains("run_command")) {
score += 20;
reasons.add("BOOK_CLICK_COMMAND");
}
if (score >= 70) {
blockedSources.put(sourceIP, Instant.now().plusSeconds(BLOCK_DURATION_SECONDS));
totalDetections.incrementAndGet();
totalBlocks.incrementAndGet();
return CrashCheckResult.detected(reasons, score);
}
return score > 0 ? CrashCheckResult.suspicious(reasons, score) : CrashCheckResult.clean();
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalDetections", totalDetections.get());
status.put("totalBlocks", totalBlocks.get());
status.put("blockedSources", blockedSources.size());
status.put("knownExploits", CRASH_EXPLOITS.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<CrashAttempt>> e : crashAttempts.entrySet()) {
for (CrashAttempt a : e.getValue()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("source", e.getKey());
m.put("packetType", a.packetType);
m.put("size", a.packetSize);
m.put("reasons", a.reasons);
m.put("time", a.timestamp.toString());
recent.add(m);
}
}
recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20)));
return status;
}
public long getTotalDetections() { return totalDetections.get(); }
public long getTotalBlocks() { return totalBlocks.get(); }
private static class CrashExploit {
final String description;
final String details;
final int severity;
final boolean autoBlock;
CrashExploit(String description, String details, int severity, boolean autoBlock) {
this.description = description;
this.details = details;
this.severity = severity;
this.autoBlock = autoBlock;
}
}
private static class CrashAttempt {
final Instant timestamp;
final String sourceIP;
final String packetType;
final int packetSize;
final List<String> reasons;
CrashAttempt(Instant timestamp, String sourceIP, String packetType, int packetSize, List<String> reasons) {
this.timestamp = timestamp;
this.sourceIP = sourceIP;
this.packetType = packetType;
this.packetSize = packetSize;
this.reasons = reasons;
}
}
public static class CrashCheckResult {
private final boolean blocked;
private final boolean detected;
private final boolean suspicious;
private final String message;
private final int score;
private final List<String> reasons;
private CrashCheckResult(boolean blocked, boolean detected, boolean suspicious, String message, int score, List<String> reasons) {
this.blocked = blocked;
this.detected = detected;
this.suspicious = suspicious;
this.message = message;
this.score = score;
this.reasons = reasons;
}
public static CrashCheckResult clean() { return new CrashCheckResult(false, false, false, null, 0, List.of()); }
public static CrashCheckResult suspicious(List<String> reasons, int score) {
return new CrashCheckResult(false, false, true, null, score, reasons); }
public static CrashCheckResult detected(List<String> reasons, int score) {
return new CrashCheckResult(true, true, true, "Crash exploit detected", score, reasons); }
public static CrashCheckResult blocked(String msg) { return new CrashCheckResult(true, false, false, msg, 0, List.of()); }
public boolean isBlocked() { return blocked; }
public boolean isDetected() { return detected; }
public boolean isSuspicious() { return suspicious; }
public String getMessage() { return message; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}
@@ -0,0 +1,238 @@
package com.aluer.security;
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;
@Service
public class LagMachineDetectionService {
private final Map<String, List<LagEvent>> lagEvents = new ConcurrentHashMap<>();
private final Map<String, Instant> flaggedPlayers = new ConcurrentHashMap<>();
private final Map<String, ChunkActivity> chunkActivity = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final long FLAG_DURATION_SECONDS = 7200;
// Lag machine indicators
private static final int MAX_ENTITIES_PER_CHUNK = 50;
private static final int MAX_TILE_ENTITIES_PER_CHUNK = 20;
private static final int MAX_REDSTONE_UPDATES_PER_CHUNK = 100;
private static final int MAX_MINECARTS_PER_CHUNK = 10;
private static final int MAX_TNT_PER_CHUNK = 30;
private static final int MAX_OBSERVER_UPDATES_PER_CHUNK = 50;
private static final int MAX_FALLING_BLOCKS = 100;
private static final Set<String> LAG_CAUSING_BLOCKS = Set.of(
"OBSERVER", "PISTON", "STICKY_PISTON", "REDSTONE_BLOCK",
"REDSTONE_WIRE", "REDSTONE_TORCH", "REPEATER", "COMPARATOR",
"HOPPER", "DISPENSER", "DROPPER", "NOTE_BLOCK", "BELL",
"TNT", "MINECART", "CHEST_MINECART", "HOPPER_MINECART",
"TNT_MINECART", "COMMAND_BLOCK", "CHAIN_COMMAND_BLOCK",
"REPEATING_COMMAND_BLOCK", "SCULK_SENSOR", "SCULK_SHRIEKER",
"CALIBRATED_SCULK_SENSOR", "DAYLIGHT_DETECTOR", "TRIPWIRE_HOOK"
);
private static final Set<String> ENTITY_LAG_TYPES = Set.of(
"ITEM_FRAME", "GLOW_ITEM_FRAME", "ARMOR_STAND", "PAINTING",
"BOAT", "CHEST_BOAT", "MINECART", "CHEST_MINECART",
"HOPPER_MINECART", "TNT_MINECART", "FURNACE_MINECART"
);
public LagMachineDetectionService() {
scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS);
}
public LagCheckResult checkBlockPlace(String playerName, String blockType, int x, int y, int z, String world) {
String chunkKey = (x >> 4) + ":" + (z >> 4) + ":" + world;
ChunkActivity chunk = chunkActivity.computeIfAbsent(chunkKey, k -> new ChunkActivity());
chunk.recordBlockPlace(blockType, playerName);
int score = 0;
List<String> reasons = new ArrayList<>();
// Rule 1: Excessive redstone components per chunk
if (LAG_CAUSING_BLOCKS.contains(blockType.toUpperCase())) {
chunk.lagBlockCount++;
if (chunk.lagBlockCount > MAX_REDSTONE_UPDATES_PER_CHUNK) {
score += 25;
reasons.add("REDSTONE_DENSITY: " + chunk.lagBlockCount + " lag blocks in chunk " + chunkKey);
}
}
// Rule 2: Observer chain detection (classic lag machine)
if ("OBSERVER".equalsIgnoreCase(blockType)) {
chunk.observerCount++;
if (chunk.observerCount > MAX_OBSERVER_UPDATES_PER_CHUNK) {
score += 35;
reasons.add("OBSERVER_CHAIN: " + chunk.observerCount + " observers in chunk");
}
}
// Rule 3: Entity spam in one area
if (ENTITY_LAG_TYPES.contains(blockType.toUpperCase())) {
chunk.entityCount++;
if (chunk.entityCount > MAX_ENTITIES_PER_CHUNK) {
score += 30;
reasons.add("ENTITY_SPAM: " + chunk.entityCount + " lag entities in chunk");
}
}
// Rule 4: TNT stacking (lag machine component)
if ("TNT".equalsIgnoreCase(blockType)) {
chunk.tntCount++;
if (chunk.tntCount > MAX_TNT_PER_CHUNK) {
score += 40;
reasons.add("TNT_STACK: " + chunk.tntCount + " TNT in chunk");
}
}
// Rule 5: Rapid placement by one player
chunk.playerBlockCount.merge(playerName, 1, Integer::sum);
int playerBlocks = chunk.playerBlockCount.getOrDefault(playerName, 0);
if (playerBlocks > 50 && LAG_CAUSING_BLOCKS.contains(blockType.toUpperCase())) {
score += 20;
reasons.add("RAPID_LAG_BUILD: " + playerBlocks + " blocks by " + playerName);
}
// Rule 6: Falling block cascade (sand/gravel lag)
if (blockType.toUpperCase().contains("SAND") || blockType.toUpperCase().contains("GRAVEL")
|| blockType.toUpperCase().contains("CONCRETE_POWDER")) {
chunk.fallingBlockSupports++;
if (chunk.fallingBlockSupports > MAX_FALLING_BLOCKS) {
score += 30;
reasons.add("FALLING_BLOCK_CASCADE: " + chunk.fallingBlockSupports);
}
}
if (score >= 40) {
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
LagEvent event = new LagEvent(Instant.now(), playerName, chunkKey, blockType, reasons);
lagEvents.computeIfAbsent(playerName, k -> new ArrayList<>()).add(event);
totalDetections.incrementAndGet();
return LagCheckResult.flagged("Lag machine detected: " + String.join("; ", reasons));
} else if (score >= 30) {
return LagCheckResult.suspicious(score, reasons);
}
return LagCheckResult.clean();
}
public LagCheckResult checkChunkTPS(double chunkTPS, String chunkKey) {
if (chunkTPS < 10.0) {
ChunkActivity chunk = chunkActivity.get(chunkKey);
if (chunk != null && chunk.lagBlockCount > 20) {
totalDetections.incrementAndGet();
return LagCheckResult.flagged("Low TPS chunk with high lag block density: " + chunkKey + " TPS=" + chunkTPS);
}
}
return LagCheckResult.clean();
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalDetections", totalDetections.get());
status.put("trackedChunks", chunkActivity.size());
status.put("flaggedPlayers", flaggedPlayers.size());
List<Map<String, Object>> topLagChunks = new ArrayList<>();
for (Map.Entry<String, ChunkActivity> e : chunkActivity.entrySet()) {
ChunkActivity c = e.getValue();
if (c.lagBlockCount > 10) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("chunk", e.getKey());
m.put("lagBlockCount", c.lagBlockCount);
m.put("observerCount", c.observerCount);
m.put("tntCount", c.tntCount);
m.put("entityCount", c.entityCount);
topLagChunks.add(m);
}
}
topLagChunks.sort((a, b) -> (int) b.get("lagBlockCount") - (int) a.get("lagBlockCount"));
status.put("topLagChunks", topLagChunks.subList(0, Math.min(topLagChunks.size(), 20)));
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<LagEvent>> e : lagEvents.entrySet()) {
for (LagEvent event : e.getValue()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("chunk", event.chunkKey);
m.put("blockType", event.blockType);
m.put("reasons", event.reasons);
m.put("time", event.timestamp.toString());
recent.add(m);
}
}
recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20)));
return status;
}
public long getTotalDetections() { return totalDetections.get(); }
private void cleanupOldData() {
Instant cutoff = Instant.now().minusSeconds(3600);
chunkActivity.entrySet().removeIf(e -> e.getValue().lastActivity.isBefore(cutoff));
flaggedPlayers.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
}
private static class ChunkActivity {
int lagBlockCount;
int observerCount;
int tntCount;
int entityCount;
int fallingBlockSupports;
final Map<String, Integer> playerBlockCount = new HashMap<>();
Instant lastActivity = Instant.now();
void recordBlockPlace(String blockType, String playerName) {
lastActivity = Instant.now();
}
}
private static class LagEvent {
final Instant timestamp;
final String playerName;
final String chunkKey;
final String blockType;
final List<String> reasons;
LagEvent(Instant timestamp, String playerName, String chunkKey, String blockType, List<String> reasons) {
this.timestamp = timestamp;
this.playerName = playerName;
this.chunkKey = chunkKey;
this.blockType = blockType;
this.reasons = reasons;
}
}
public static class LagCheckResult {
private final boolean flagged;
private final boolean suspicious;
private final int score;
private final List<String> reasons;
private LagCheckResult(boolean flagged, boolean suspicious, int score, List<String> reasons) {
this.flagged = flagged;
this.suspicious = suspicious;
this.score = score;
this.reasons = reasons;
}
public static LagCheckResult clean() { return new LagCheckResult(false, false, 0, List.of()); }
public static LagCheckResult suspicious(int score, List<String> reasons) { return new LagCheckResult(false, true, score, reasons); }
public static LagCheckResult flagged(String msg) { return new LagCheckResult(true, true, 100, List.of(msg)); }
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}
@@ -55,6 +55,12 @@ public class DashboardController {
private final ThreatHuntingService threatHuntingService; private final ThreatHuntingService threatHuntingService;
private final ComplianceScannerService complianceScannerService; private final ComplianceScannerService complianceScannerService;
private final AntiGriefDetectionService antiGriefDetectionService; private final AntiGriefDetectionService antiGriefDetectionService;
// Minecraft super-evolution services
private final AntiXrayDetectionService antiXrayDetectionService;
private final AntiFlyDetectionService antiFlyDetectionService;
private final AntiDupeDetectionService antiDupeDetectionService;
private final CrashExploitProtectionService crashExploitProtectionService;
private final LagMachineDetectionService lagMachineDetectionService;
@SuppressWarnings("java:S107") @SuppressWarnings("java:S107")
public DashboardController( public DashboardController(
@@ -89,7 +95,12 @@ public class DashboardController {
IncidentResponseService incidentResponseService, IncidentResponseService incidentResponseService,
ThreatHuntingService threatHuntingService, ThreatHuntingService threatHuntingService,
ComplianceScannerService complianceScannerService, ComplianceScannerService complianceScannerService,
AntiGriefDetectionService antiGriefDetectionService) { AntiGriefDetectionService antiGriefDetectionService,
AntiXrayDetectionService antiXrayDetectionService,
AntiFlyDetectionService antiFlyDetectionService,
AntiDupeDetectionService antiDupeDetectionService,
CrashExploitProtectionService crashExploitProtectionService,
LagMachineDetectionService lagMachineDetectionService) {
this.rconClient = rconClient; this.rconClient = rconClient;
this.profiler = profiler; this.profiler = profiler;
this.backupService = backupService; this.backupService = backupService;
@@ -122,6 +133,11 @@ public class DashboardController {
this.threatHuntingService = threatHuntingService; this.threatHuntingService = threatHuntingService;
this.complianceScannerService = complianceScannerService; this.complianceScannerService = complianceScannerService;
this.antiGriefDetectionService = antiGriefDetectionService; this.antiGriefDetectionService = antiGriefDetectionService;
this.antiXrayDetectionService = antiXrayDetectionService;
this.antiFlyDetectionService = antiFlyDetectionService;
this.antiDupeDetectionService = antiDupeDetectionService;
this.crashExploitProtectionService = crashExploitProtectionService;
this.lagMachineDetectionService = lagMachineDetectionService;
} }
@GetMapping("/status") @GetMapping("/status")
@@ -424,4 +440,31 @@ public class DashboardController {
public Map<String, Object> getAntiGriefStatus() { public Map<String, Object> getAntiGriefStatus() {
return antiGriefDetectionService.getStatus(); return antiGriefDetectionService.getStatus();
} }
// --- Minecraft专项安全 API ---
@GetMapping("/security/anti-xray/status")
public Map<String, Object> getAntiXrayStatus() {
return antiXrayDetectionService.getStatus();
}
@GetMapping("/security/anti-fly/status")
public Map<String, Object> getAntiFlyStatus() {
return antiFlyDetectionService.getStatus();
}
@GetMapping("/security/anti-dupe/status")
public Map<String, Object> getAntiDupeStatus() {
return antiDupeDetectionService.getStatus();
}
@GetMapping("/security/crash-exploit/status")
public Map<String, Object> getCrashExploitStatus() {
return crashExploitProtectionService.getStatus();
}
@GetMapping("/security/lag-machine/status")
public Map<String, Object> getLagMachineStatus() {
return lagMachineDetectionService.getStatus();
}
} }
@@ -0,0 +1,221 @@
package com.aluer.security;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class MinecraftSecurityTest {
// --- AntiXrayDetectionService Tests ---
@Test
void antiXrayDetectsHighDiamondRatio() {
AntiXrayDetectionService xray = new AntiXrayDetectionService();
// Mine 50 "valuable ores" and only 30 "common blocks" = ratio 1.67 > 0.5
for (int i = 0; i < 30; i++) {
xray.checkBlockBreak("XrayPlayer", "STONE", i, 10, 0, "world");
}
for (int i = 0; i < 20; i++) {
AntiXrayDetectionService.XrayCheckResult result = xray.checkBlockBreak(
"XrayPlayer", "DIAMOND_ORE", 100, 12, 100, "world");
if (result.isFlagged()) break;
}
var status = xray.getStatus();
assertTrue((Long) status.get("totalDetections") > 0);
}
@Test
void antiXrayAllowsNormalMining() {
AntiXrayDetectionService xray = new AntiXrayDetectionService();
// Normal mining pattern: lots of stone, few ores
for (int i = 0; i < 100; i++) {
xray.checkBlockBreak("GoodMiner", "STONE", i, 10, 0, "world");
}
AntiXrayDetectionService.XrayCheckResult result = xray.checkBlockBreak(
"GoodMiner", "DIAMOND_ORE", 50, 12, 50, "world");
assertFalse(result.isFlagged());
}
@Test
void antiXrayClearPlayerResets() {
AntiXrayDetectionService xray = new AntiXrayDetectionService();
for (int i = 0; i < 60; i++) {
xray.checkBlockBreak("TestPlayer", "DIAMOND_ORE", 100, 12, i, "world");
}
xray.clearPlayer("TestPlayer");
AntiXrayDetectionService.XrayCheckResult result = xray.checkBlockBreak(
"TestPlayer", "STONE", 0, 10, 0, "world");
assertFalse(result.isFlagged());
}
// --- AntiFlyDetectionService Tests ---
@Test
void antiFlyDetectsImpossibleVertical() {
AntiFlyDetectionService fly = new AntiFlyDetectionService();
// Rapid upward movement without ground contact (flying up 10 blocks instantly)
AntiFlyDetectionService.FlyCheckResult result = fly.checkMovement(
"FlyHacker", 0, 64, 0, 0, 75, 0, false, "survival");
assertTrue(result.isFlagged());
}
@Test
void antiFlyDetectsExcessiveHorizontal() {
AntiFlyDetectionService fly = new AntiFlyDetectionService();
// Moving 20 blocks in one tick in survival mode
AntiFlyDetectionService.FlyCheckResult result = fly.checkMovement(
"SpeedHacker", 0, 64, 0, 20, 64, 0, true, "survival");
assertTrue(result.isFlagged());
}
@Test
void antiFlyDetectsTeleport() {
AntiFlyDetectionService fly = new AntiFlyDetectionService();
// 100-block teleport in survival
AntiFlyDetectionService.FlyCheckResult result = fly.checkMovement(
"Teleporter", 0, 64, 0, 100, 64, 0, true, "survival");
assertTrue(result.isFlagged());
}
@Test
void antiFlyAllowsNormalMovement() {
AntiFlyDetectionService fly = new AntiFlyDetectionService();
AntiFlyDetectionService.FlyCheckResult result = fly.checkMovement(
"NormalPlayer", 0, 64, 0, 0.3, 64, 0.3, true, "survival");
assertFalse(result.isFlagged());
}
@Test
void antiFlyAllowsCreativeFlight() {
AntiFlyDetectionService fly = new AntiFlyDetectionService();
AntiFlyDetectionService.FlyCheckResult result = fly.checkMovement(
"CreativePlayer", 0, 64, 0, 5, 68, 5, false, "creative");
assertFalse(result.isFlagged());
}
// --- AntiDupeDetectionService Tests ---
@Test
void antiDupeDetectsStackOverflow() {
AntiDupeDetectionService dupe = new AntiDupeDetectionService();
// 100 diamonds in one stack (impossible - max is 64)
AntiDupeDetectionService.DupeCheckResult result = dupe.checkInventoryChange(
"DupeHacker", "DIAMOND", 100, 10, "CHEST", 0, 64, 0);
assertTrue(result.isFlagged());
}
@Test
void antiDupeDetectsNetheriteSurge() {
AntiDupeDetectionService dupe = new AntiDupeDetectionService();
// 200 netherite blocks gained in one transaction
AntiDupeDetectionService.DupeCheckResult result = dupe.checkInventoryChange(
"DupeHacker2", "NETHERITE_BLOCK", 200, 0, "SHULKER_BOX", 0, 64, 0);
assertTrue(result.isFlagged());
}
@Test
void antiDupeDetectsImpossibleIncrease() {
AntiDupeDetectionService dupe = new AntiDupeDetectionService();
// 1 -> 600 items (3x increase + >500)
AntiDupeDetectionService.DupeCheckResult result = dupe.checkInventoryChange(
"DupeHacker3", "DIAMOND", 600, 1, "CHEST", 0, 64, 0);
assertTrue(result.isFlagged());
}
@Test
void antiDupeAllowsNormalTransfer() {
AntiDupeDetectionService dupe = new AntiDupeDetectionService();
AntiDupeDetectionService.DupeCheckResult result = dupe.checkInventoryChange(
"NormalPlayer", "DIAMOND", 40, 30, "CHEST", 0, 64, 0);
assertFalse(result.isFlagged());
}
// --- CrashExploitProtectionService Tests ---
@Test
void crashExploitDetectsOversizedPacket() {
CrashExploitProtectionService crash = new CrashExploitProtectionService();
CrashExploitProtectionService.CrashCheckResult result = crash.checkPacket(
"CustomPayload", 3000000, "10.0.0.1"); // 3MB packet
assertTrue(result.isDetected() || result.isBlocked());
}
@Test
void crashExploitDetectsNegativeLength() {
CrashExploitProtectionService crash = new CrashExploitProtectionService();
CrashExploitProtectionService.CrashCheckResult result = crash.checkPacket(
"Login", -1, "10.0.0.2");
assertTrue(result.isDetected() || result.isBlocked());
}
@Test
void crashExploitDetectsDeepNesting() {
CrashExploitProtectionService crash = new CrashExploitProtectionService();
CrashExploitProtectionService.CrashCheckResult result = crash.checkNBTData(
"deeply nested nbt data", 100, "10.0.0.3");
assertTrue(result.isDetected() || result.isBlocked());
}
@Test
void crashExploitDetectsOversizedBook() {
CrashExploitProtectionService crash = new CrashExploitProtectionService();
CrashExploitProtectionService.CrashCheckResult result = crash.checkBookContent(
"{\"text\":\"...large...\"}", 150, 50000, "10.0.0.4");
assertTrue(result.isDetected() || result.isBlocked());
}
@Test
void crashExploitAllowsNormalPacket() {
CrashExploitProtectionService crash = new CrashExploitProtectionService();
CrashExploitProtectionService.CrashCheckResult result = crash.checkPacket(
"KeepAlive", 64, "10.0.0.5");
assertFalse(result.isDetected());
}
// --- LagMachineDetectionService Tests ---
@Test
void lagMachineDetectsObserverChain() {
LagMachineDetectionService lag = new LagMachineDetectionService();
// Place 60 observers + 50 TNT in same chunk = both rules trigger, score >= 40
LagMachineDetectionService.LagCheckResult result = null;
for (int i = 0; i < 60; i++) {
result = lag.checkBlockPlace("LagBuilder", "OBSERVER", i % 16, 64, 0, "world");
}
for (int i = 0; i < 50; i++) {
result = lag.checkBlockPlace("LagBuilder", "TNT", i % 16, 64, 0, "world");
}
assertNotNull(result);
assertTrue(result.isFlagged());
}
@Test
void lagMachineDetectsTNTStack() {
LagMachineDetectionService lag = new LagMachineDetectionService();
LagMachineDetectionService.LagCheckResult result = null;
for (int i = 0; i < 50; i++) {
result = lag.checkBlockPlace("TNTBuilder", "TNT", 0, 64, i % 16, "world");
if (result != null && result.isFlagged()) break;
}
assertNotNull(result);
assertTrue(result.isFlagged());
}
@Test
void lagMachineAllowsNormalBuilding() {
LagMachineDetectionService lag = new LagMachineDetectionService();
LagMachineDetectionService.LagCheckResult result = lag.checkBlockPlace(
"Builder", "STONE_BRICKS", 8, 64, 8, "world");
assertFalse(result.isFlagged());
}
@Test
void lagMachineDetectsHighLagDensity() {
LagMachineDetectionService lag = new LagMachineDetectionService();
LagMachineDetectionService.LagCheckResult result = null;
// Place 105 redstone-related blocks in one chunk
String[] lagBlocks = {"REDSTONE_WIRE", "REPEATER", "OBSERVER", "PISTON", "HOPPER"};
int count = 0;
for (int i = 0; i < 110 && count < 5; i++) {
result = lag.checkBlockPlace("RedstoneGuy", lagBlocks[i % 5], i % 16, 64, i % 16, "world");
if (result != null && result.isFlagged()) break;
}
assertNotNull(result);
assertTrue(result.isFlagged() || result.isSuspicious());
}
}