Files
Aluer/src/main/java/com/aluer/defense/ConnectionHandshakeValidator.java
T
Peijun Zhao 055de65e88 安全模块大规模重构:151文件从com.aluer.security分包到8个子包
新包结构:
- com.aluer.anticheat.combat (16) — KillAura/Reach/Criticals/CrystalAura/Totem等战斗反作弊
- com.aluer.anticheat.movement (18) — Fly/Speed/Jesus/NoFall/Timer/Phase等移动反作弊
- com.aluer.anticheat.world (14) — Nuker/Xray/AutoMine/VeinMiner等世界反作弊
- com.aluer.anticheat.player (8) — FakePlayer/AltAccount/NameSpoof等玩家反作弊
- com.aluer.network (32) — DDoS/Protocol/TokenBucket/Firewall等网络安全
- com.aluer.server (10) — ChunkLoad/EntityCount/Redstone/CrashExploit等服务器保护
- com.aluer.chat (5) — ChatFlood/Advertisement/Phishing/CommandAbuse等聊天安全
- com.aluer.defense (50) — WAF/IDS/IPS/Honeypot/EDR/Forensics等纵深防御

所有package声明+跨包import已更新,323测试全绿
2026-05-22 19:29:45 +08:00

558 lines
20 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.aluer.defense;
import com.aluer.config.ServerGuardConfig;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Minecraft连接握手验证器 — 检测握手阶段的异常行为和攻击探测
*
* 检测原理:
* Minecraft客户端在连接到服务器时首先发送一个握手包(Handshake packet),其中包含:
* - Protocol Version: 客户端支持的协议版本号
* - Server Address: 目标服务器地址(用于虚拟主机识别)
* - Server Port: 目标端口
* - Next State: 下一个状态(1=STATUS服务器列表查询,2=LOGIN登录)
*
* 攻击者通过在握手阶段发送异常数据来探测或攻击服务器:
*
* 1. 协议版本异常 — 发送不存在或极端的协议版本号(如-1、99999),试图触发
* PaperMC的未知协议处理逻辑漏洞
* 2. 主机名恶意构造 — 超长或畸形的主机名字段,可能导致日志注入或缓冲区溢出
* 3. Next State 异常 — 发送超过标准范围(1-2)的nextState值,
* 试图触发意外的状态机行为
* 4. Ping洪水探测 — 大量快速的STATUS请求(服务器列表ping),
* 攻击者用此扫描开放的Minecraft服务器
* 5. 握手超时 — 发送握手包后长时间不继续后续流程,
* 占用连接槽位(类似SlowLoris攻击)
* 6. 端口扫描 — 在不同端口上发送握手包以确认服务器存在(跨端口探测)
*
* 配置开关:serverguard.security.super-evolution.handshake-validator
*/
@Service
public class ConnectionHandshakeValidator {
private final ServerGuardConfig config;
/**
* 追踪每个IP的握手状态(IP → 握手跟踪记录)
* 记录未完成的握手以检测超时
*/
private final Map<String, HandshakeRecord> pendingHandshakes = new ConcurrentHashMap<>();
/**
* 追踪每个IP的握手完成率(IP → 统计记录)
* 正常玩家的完成率接近100%,扫描器的完成率极低
*/
private final Map<String, ConnectionStats> connectionStats = new ConcurrentHashMap<>();
/**
* 追踪每个IP的服务器列表Ping记录(IP → ping时间戳列表)
* 用于检测Ping洪水
*/
private final Map<String, List<Long>> pingTimestamps = new ConcurrentHashMap<>();
/**
* 封禁IP及其封禁到期时间(IP → 解除封禁的毫秒时间戳)
*/
private final Map<String, Long> blockedIPs = new ConcurrentHashMap<>();
private final AtomicLong totalHandshakes = new AtomicLong(0);
private final AtomicLong totalCompletions = new AtomicLong(0);
private final AtomicLong totalViolations = new AtomicLong(0);
private final AtomicLong totalBlocks = new AtomicLong(0);
/**
* Minecraft支持的协议版本范围
* PaperMC 1.21.1 的协议版本约为 767
*/
private static final int MIN_PROTOCOL_VERSION = 750;
private static final int MAX_PROTOCOL_VERSION = 770;
/**
* nextState字段合法值
*/
private static final int NEXT_STATE_STATUS = 1;
private static final int NEXT_STATE_LOGIN = 2;
/**
* 主机名最大长度 — 正常域名不超过253字符,FQDN限制
* 超过此值可能是缓冲区溢出探测
*/
private static final int MAX_HOSTNAME_LENGTH = 255;
/**
* Ping洪水检测阈值 — 在检测窗口内超过此数量的Ping视为扫描
*/
private static final int PING_FLOOD_THRESHOLD = 30;
/**
* Ping洪水检测窗口(毫秒)— 10秒
*/
private static final long PING_FLOOD_WINDOW_MS = 10_000;
/**
* 握手超时时间(毫秒)— 发送握手包后30秒未完成登录视为超时
*/
private static final long HANDSHAKE_TIMEOUT_MS = 30_000;
/**
* 握手完成率阈值 — 低于此比例的IP视为可疑扫描器
* 扫描器通常只有ping没有login,完成率接近于0
*/
private static final double MIN_COMPLETION_RATE = 0.1;
/**
* 最低握手次数(用于完成率评估)— 至少需要此数量的握手才能评估完成率
*/
private static final int MIN_HANDSHAKES_FOR_EVAL = 5;
/**
* 端口扫描检测 — 同一IP在多个端口上的握手
*/
private static final int PORT_SCAN_THRESHOLD = 3;
/**
* Ping间隔过短阈值 — 两次Ping间隔低于此值视为异常
*/
private static final long ABNORMAL_PING_INTERVAL_MS = 200;
/**
* 封禁时长(毫秒)— 默认30分钟
*/
private static final long BLOCK_DURATION_MS = 30 * 60 * 1000L;
/**
* 无参构造函数 — 测试/默认配置使用
*/
public ConnectionHandshakeValidator() {
this(new ServerGuardConfig());
}
/**
* Spring 注入构造函数
* @param config 全局配置,提供模块开关控制
*/
public ConnectionHandshakeValidator(ServerGuardConfig config) {
this.config = config;
}
/**
* 记录握手包接收事件
*
* @param ip 客户端IP地址
* @param protocolVersion 客户端声明的协议版本
* @param nextState 握手包中声明的nextState(1=STATUS, 2=LOGIN)
* @param hostname 握手包中声明的目标主机名
* @param timestamp 握手时间戳
* @return 验证结果
*/
public HandshakeValidationResult recordHandshake(String ip, int protocolVersion,
int nextState, String hostname,
Instant timestamp) {
totalHandshakes.incrementAndGet();
if (!config.getSecurity().getSuperEvolution().isHandshakeValidator()) {
return HandshakeValidationResult.clean();
}
// 检查IP是否已被封禁
if (isBlocked(ip, timestamp)) {
return HandshakeValidationResult.blocked("IP is temporarily blocked for handshake anomalies");
}
List<String> reasons = new ArrayList<>();
// 1. 验证协议版本
if (protocolVersion < MIN_PROTOCOL_VERSION || protocolVersion > MAX_PROTOCOL_VERSION) {
reasons.add("ABNORMAL_PROTOCOL_VERSION: " + protocolVersion
+ " (expected " + MIN_PROTOCOL_VERSION + "~" + MAX_PROTOCOL_VERSION + ")");
}
// 2. 验证 nextState 字段
if (nextState != NEXT_STATE_STATUS && nextState != NEXT_STATE_LOGIN) {
reasons.add("INVALID_NEXT_STATE: " + nextState
+ " (expected 1=STATUS or 2=LOGIN) — possible protocol fuzzing");
}
// 3. 验证主机名长度
if (hostname != null && hostname.length() > MAX_HOSTNAME_LENGTH) {
reasons.add("OVERSIZED_HOSTNAME: " + hostname.length() + " chars > max=" + MAX_HOSTNAME_LENGTH
+ " — possible buffer overflow probe");
}
// 4. 检测主机名中的可疑字符(空字符注入等)
if (hostname != null && (hostname.contains("\0") || hostname.contains("\n") || hostname.contains("\r"))) {
reasons.add("MALFORMED_HOSTNAME: contains null/newline characters — possible injection attempt");
}
// 5. 记录握手等待(用于超时检测)
HandshakeRecord record = new HandshakeRecord(protocolVersion, nextState, hostname, timestamp);
pendingHandshakes.put(ip, record);
// 6. 更新连接统计
ConnectionStats stats = connectionStats.computeIfAbsent(ip, k -> new ConnectionStats());
stats.totalHandshakes++;
if (!reasons.isEmpty()) {
totalViolations.addAndGet(reasons.size());
// 检查是否需要封禁
stats.violationCount++;
if (stats.violationCount >= 5) {
blockIP(ip, timestamp, "Repeated handshake violations: " + stats.violationCount);
return HandshakeValidationResult.blocked(reasons);
}
return HandshakeValidationResult.flagged(reasons);
}
return HandshakeValidationResult.clean();
}
/**
* 记录玩家登录成功(握手完成)事件
*
* @param ip 客户端IP地址
* @param playerName 登录成功的玩家名称
* @param timestamp 完成时间戳
*/
public void recordHandshakeCompletion(String ip, String playerName, Instant timestamp) {
if (!config.getSecurity().getSuperEvolution().isHandshakeValidator()) {
return;
}
totalCompletions.incrementAndGet();
// 移除等待中的握手记录
pendingHandshakes.remove(ip);
// 更新连接统计
ConnectionStats stats = connectionStats.get(ip);
if (stats != null) {
stats.successfulLogins++;
}
// 清理过期的ping记录
cleanupPingTimestamps(ip, timestamp.toEpochMilli());
}
/**
* 记录服务器列表Ping事件
* 在短时间内大量Ping是端口探测的典型特征
*
* @param ip 客户端IP地址
* @param port 目标端口
* @param timestamp Ping时间戳
* @return 验证结果
*/
public HandshakeValidationResult recordServerListPing(String ip, int port, Instant timestamp) {
if (!config.getSecurity().getSuperEvolution().isHandshakeValidator()) {
return HandshakeValidationResult.clean();
}
if (isBlocked(ip, timestamp)) {
return HandshakeValidationResult.blocked("IP is temporarily blocked for handshake anomalies");
}
List<String> reasons = new ArrayList<>();
long nowMs = timestamp.toEpochMilli();
// 记录Ping时间戳
List<Long> timestamps = pingTimestamps.computeIfAbsent(ip,
k -> Collections.synchronizedList(new ArrayList<>()));
timestamps.add(nowMs);
// 检测Ping洪水 — 在窗口内超过阈值
long cutoff = nowMs - PING_FLOOD_WINDOW_MS;
long recentPings = timestamps.stream().filter(t -> t > cutoff).count();
if (recentPings > PING_FLOOD_THRESHOLD) {
reasons.add("PING_FLOOD: " + recentPings + " pings in " + PING_FLOOD_WINDOW_MS / 1000
+ "s (threshold: " + PING_FLOOD_THRESHOLD + ")");
}
// 检测Ping间隔过于规律 — 自动化扫描特征
if (timestamps.size() >= 5) {
// 取最近5个时间戳计算间隔
int size = timestamps.size();
List<Long> recent = new ArrayList<>(timestamps.subList(Math.max(0, size - 6), size));
if (recent.size() >= 3) {
long minInterval = Long.MAX_VALUE;
long maxInterval = Long.MIN_VALUE;
for (int i = 1; i < recent.size(); i++) {
long interval = recent.get(i) - recent.get(i - 1);
minInterval = Math.min(minInterval, interval);
maxInterval = Math.max(maxInterval, interval);
}
// 间隔一致性过高 → 脚本化扫描
if (maxInterval - minInterval < ABNORMAL_PING_INTERVAL_MS && recent.size() >= 5) {
reasons.add("AUTOMATED_PING_PATTERN: ping intervals "
+ minInterval + "~" + maxInterval + "ms — automated scanner detected");
}
}
}
// 检测端口扫描 — 同一IP在多个端口上ping
if (port != 25565) {
ConnectionStats stats = connectionStats.computeIfAbsent(ip, k -> new ConnectionStats());
stats.scannedPorts.add(port);
if (stats.scannedPorts.size() >= PORT_SCAN_THRESHOLD) {
reasons.add("PORT_SCAN: " + stats.scannedPorts.size() + " different ports probed: "
+ stats.scannedPorts);
}
}
// 清理过期时间戳
cleanupPingTimestamps(ip, nowMs);
if (!reasons.isEmpty()) {
totalViolations.addAndGet(reasons.size());
ConnectionStats stats = connectionStats.computeIfAbsent(ip, k -> new ConnectionStats());
stats.violationCount++;
if (stats.violationCount >= 3) {
blockIP(ip, timestamp, "Port scan / ping flood: " + stats.violationCount + " violations");
return HandshakeValidationResult.blocked(reasons);
}
return HandshakeValidationResult.flagged(reasons);
}
return HandshakeValidationResult.clean();
}
/**
* 检测超时的握手 — 应在外部定时调用(如每15秒一次)
* 握手包发送后30秒内未完成登录的连接将被标记
*
* @param now 当前时间戳
* @return 超时握手的IP集合
*/
public Set<String> detectHandshakeTimeouts(Instant now) {
if (!config.getSecurity().getSuperEvolution().isHandshakeValidator()) {
return Set.of();
}
Set<String> timeoutIPs = new HashSet<>();
long cutoff = now.toEpochMilli() - HANDSHAKE_TIMEOUT_MS;
for (Map.Entry<String, HandshakeRecord> entry : pendingHandshakes.entrySet()) {
HandshakeRecord record = entry.getValue();
if (record.timestamp.toEpochMilli() < cutoff) {
// 握手超时 — 占用连接槽位但不完成登录,类似SlowLoris行为
timeoutIPs.add(entry.getKey());
totalViolations.incrementAndGet();
ConnectionStats stats = connectionStats.computeIfAbsent(entry.getKey(), k -> new ConnectionStats());
stats.timeoutCount++;
if (stats.timeoutCount >= 5) {
blockIP(entry.getKey(), now, "Repeated handshake timeouts: " + stats.timeoutCount);
}
}
}
// 清理已超时的记录
for (String ip : timeoutIPs) {
pendingHandshakes.remove(ip);
}
return timeoutIPs;
}
/**
* 检查IP的握手完成率是否异常
* 扫描器的完成率极低(大量Ping但几乎不登录)
*
* @param ip 要检查的IP地址
* @return 完成率是否异常(false=异常低,true=正常)
*/
public boolean hasNormalCompletionRate(String ip) {
if (!config.getSecurity().getSuperEvolution().isHandshakeValidator()) {
return true;
}
ConnectionStats stats = connectionStats.get(ip);
if (stats == null || stats.totalHandshakes < MIN_HANDSHAKES_FOR_EVAL) {
return true; // 样本不足,放行
}
double rate = (double) stats.successfulLogins / stats.totalHandshakes;
return rate >= MIN_COMPLETION_RATE;
}
/**
* 获取指定IP的连接统计信息
*
* @param ip 要查询的IP地址
* @return 连接统计,无记录时返回null
*/
public ConnectionStats getConnectionStats(String ip) {
return connectionStats.get(ip);
}
/**
* 重置指定IP的所有追踪数据
*
* @param ip 要重置的IP地址
*/
public void resetIP(String ip) {
pendingHandshakes.remove(ip);
connectionStats.remove(ip);
pingTimestamps.remove(ip);
blockedIPs.remove(ip);
}
/**
* 获取模块运行状态
*
* @return 包含计数器、活跃追踪IP数、封禁IP数的Map
*/
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalHandshakes", totalHandshakes.get());
status.put("totalCompletions", totalCompletions.get());
status.put("totalViolations", totalViolations.get());
status.put("totalBlocks", totalBlocks.get());
status.put("pendingHandshakes", pendingHandshakes.size());
status.put("trackedIPs", connectionStats.size());
status.put("blockedIPs", blockedIPs.size());
double completionRate = 0;
long total = totalHandshakes.get();
if (total > 0) {
completionRate = (double) totalCompletions.get() / total * 100.0;
}
status.put("globalCompletionRate", String.format("%.2f%%", completionRate));
return status;
}
/**
* 检查IP是否处于封禁状态
*/
private boolean isBlocked(String ip, Instant now) {
Long unblockTime = blockedIPs.get(ip);
if (unblockTime != null) {
if (now.toEpochMilli() < unblockTime) {
return true;
}
blockedIPs.remove(ip);
}
return false;
}
/**
* 封禁指定IP
*/
private void blockIP(String ip, Instant timestamp, String reason) {
blockedIPs.put(ip, timestamp.toEpochMilli() + BLOCK_DURATION_MS);
totalBlocks.incrementAndGet();
}
/**
* 清理过期的Ping时间戳记录
*/
private void cleanupPingTimestamps(String ip, long nowMs) {
List<Long> timestamps = pingTimestamps.get(ip);
if (timestamps != null) {
long cutoff = nowMs - PING_FLOOD_WINDOW_MS * 3; // 保留3倍窗口数据
synchronized (timestamps) {
timestamps.removeIf(t -> t < cutoff);
}
if (timestamps.isEmpty()) {
pingTimestamps.remove(ip);
}
}
}
/**
* 内部握手跟踪记录 — 记录未完成握手的客户端信息
*/
private static class HandshakeRecord {
final int protocolVersion;
final int nextState;
final String hostname;
final Instant timestamp;
HandshakeRecord(int protocolVersion, int nextState, String hostname, Instant timestamp) {
this.protocolVersion = protocolVersion;
this.nextState = nextState;
this.hostname = hostname;
this.timestamp = timestamp;
}
}
/**
* IP连接统计 — 追踪每个IP的握手行为和违规历史
*/
public static class ConnectionStats {
public int totalHandshakes = 0;
public int successfulLogins = 0;
public int violationCount = 0;
public int timeoutCount = 0;
public final Set<Integer> scannedPorts = new HashSet<>();
/**
* 计算握手完成率
* @return 0.0 ~ 1.0,样本不足时返回-1
*/
public double getCompletionRate() {
if (totalHandshakes < MIN_HANDSHAKES_FOR_EVAL) return -1;
return (double) successfulLogins / totalHandshakes;
}
/**
* 判断是否为可疑扫描器(低完成率 + 多端口扫描 + 高违规)
*/
public boolean isSuspiciousScanner() {
double rate = getCompletionRate();
return (rate >= 0 && rate < MIN_COMPLETION_RATE)
|| scannedPorts.size() >= PORT_SCAN_THRESHOLD
|| violationCount >= 3;
}
}
/**
* 握手验证结果 — 不可变结果类
*/
public static class HandshakeValidationResult {
private final boolean clean;
private final boolean flagged;
private final boolean blocked;
private final List<String> reasons;
private HandshakeValidationResult(boolean clean, boolean flagged, boolean blocked, List<String> reasons) {
this.clean = clean;
this.flagged = flagged;
this.blocked = blocked;
this.reasons = reasons;
}
/** 无异常 — 握手数据正常 */
public static HandshakeValidationResult clean() {
return new HandshakeValidationResult(true, false, false, List.of());
}
/** 已标记 — 握手数据可疑,但未达封禁阈值 */
public static HandshakeValidationResult flagged(List<String> reasons) {
return new HandshakeValidationResult(false, true, false, reasons);
}
/** 已封禁 — 多次违规或严重异常,IP被临时封禁 */
public static HandshakeValidationResult blocked(List<String> reasons) {
return new HandshakeValidationResult(false, false, true, reasons);
}
/** 带自定义消息的封禁结果 — 用于已封禁IP的快捷返回 */
private static HandshakeValidationResult blocked(String reason) {
return new HandshakeValidationResult(false, false, true, List.of(reason));
}
public boolean isClean() { return clean; }
public boolean isFlagged() { return flagged; }
public boolean isBlocked() { return blocked; }
public List<String> getReasons() { return reasons; }
}
}