超进化 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:
@@ -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 ComplianceScannerService complianceScannerService;
|
||||
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")
|
||||
public DashboardController(
|
||||
@@ -89,7 +95,12 @@ public class DashboardController {
|
||||
IncidentResponseService incidentResponseService,
|
||||
ThreatHuntingService threatHuntingService,
|
||||
ComplianceScannerService complianceScannerService,
|
||||
AntiGriefDetectionService antiGriefDetectionService) {
|
||||
AntiGriefDetectionService antiGriefDetectionService,
|
||||
AntiXrayDetectionService antiXrayDetectionService,
|
||||
AntiFlyDetectionService antiFlyDetectionService,
|
||||
AntiDupeDetectionService antiDupeDetectionService,
|
||||
CrashExploitProtectionService crashExploitProtectionService,
|
||||
LagMachineDetectionService lagMachineDetectionService) {
|
||||
this.rconClient = rconClient;
|
||||
this.profiler = profiler;
|
||||
this.backupService = backupService;
|
||||
@@ -122,6 +133,11 @@ public class DashboardController {
|
||||
this.threatHuntingService = threatHuntingService;
|
||||
this.complianceScannerService = complianceScannerService;
|
||||
this.antiGriefDetectionService = antiGriefDetectionService;
|
||||
this.antiXrayDetectionService = antiXrayDetectionService;
|
||||
this.antiFlyDetectionService = antiFlyDetectionService;
|
||||
this.antiDupeDetectionService = antiDupeDetectionService;
|
||||
this.crashExploitProtectionService = crashExploitProtectionService;
|
||||
this.lagMachineDetectionService = lagMachineDetectionService;
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
@@ -424,4 +440,31 @@ public class DashboardController {
|
||||
public Map<String, Object> getAntiGriefStatus() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user