v0.3: 6-layer architecture complete

Layers:
  Layer 1: XDP/eBPF — TCP state machine, SYN throttle, blacklist, ringbuf
  Layer 2: PoW Challenge — SHA-256 hashcash, dynamic difficulty, constant-time verify
  Layer 3: Rust Core — HMAC handshake, rate limit, death code (existing)
  Layer 4: Velocity — Physics check, CAPTCHA, protocol verification
  Layer 5: Paper — Heartbeat, auto-registration (existing)
  Layer 6: Traffic Intel — EWMA, 168h profiling, reputation, alerts

Infra: XDP→Prometheus metrics, ClickHouse + Grafana dashboard, Docker Compose
Testing: 100-IP DDoS simulation, MHDDoS ref analysis, load test report
Fixes: VarInt sign extension UB, pure ACK deadlock, RST/FIN cleanup
Ref: MHDDoS, Sonar, LimboFilter, AtomGuard, Infrarust, MC-XDP-eBPF, PowGo
This commit is contained in:
loki5512344 2026-07-21 15:47:36 +02:00
parent 78fc6e00c7
commit 269daa071f
Signed by: boba
GPG key ID: 253067914055423B
66 changed files with 4529 additions and 1003 deletions

View file

@ -0,0 +1,111 @@
package me.rampart.velocity;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import net.kyori.adventure.text.Component;
import org.slf4j.Logger;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class CaptchaHandler {
private static final int MAX_ATTEMPTS = 3;
private static final long EXPIRY_MS = 30_000;
private final Logger logger;
private final ProxyServer server;
private final Map<UUID, CaptchaSession> pending = new ConcurrentHashMap<>();
private final Random random = new Random();
public CaptchaHandler(Logger logger, ProxyServer server) {
this.logger = logger;
this.server = server;
}
public void challenge(Player player) {
int token = 1000 + random.nextInt(9000);
pending.put(player.getUniqueId(), new CaptchaSession(token, 0, System.currentTimeMillis() + EXPIRY_MS));
player.sendMessage(Component.text("§e[Rampart] Please type §6/verify " + token + " §eto verify you are not a bot."));
logger.info("CAPTCHA challenge sent to {}", player.getUsername());
}
@Subscribe
public void onPlayerChat(PlayerChatEvent event) {
Player player = event.getPlayer();
CaptchaSession session = pending.get(player.getUniqueId());
if (session == null) return;
if (System.currentTimeMillis() > session.expiry) {
pending.remove(player.getUniqueId());
player.disconnect(Component.text("CAPTCHA timed out"));
logger.info("CAPTCHA timed out for {}", player.getUsername());
return;
}
String message = event.getMessage().trim();
if (message.startsWith("/verify ")) {
event.setResult(PlayerChatEvent.ChatResult.denied());
String[] parts = message.split(" ");
if (parts.length == 2) {
try {
int guess = Integer.parseInt(parts[1]);
if (guess == session.token) {
pending.remove(player.getUniqueId());
player.sendMessage(Component.text("§a[Rampart] You have been verified."));
logger.info("CAPTCHA passed for {}", player.getUsername());
HmacCheckListener.markVerified(player.getUniqueId());
PhysicsCheckListener.clearSuspicion(player.getUniqueId());
return;
}
} catch (NumberFormatException ignored) {
}
}
session.attempts++;
if (session.attempts >= MAX_ATTEMPTS) {
pending.remove(player.getUniqueId());
player.disconnect(Component.text("CAPTCHA failed"));
logger.warn("CAPTCHA failed for {}", player.getUsername());
} else {
player.sendMessage(Component.text("§c[Rampart] Incorrect. Attempts remaining: " + (MAX_ATTEMPTS - session.attempts)));
}
}
}
@Subscribe
public void onCommand(CommandExecuteEvent event) {
if (!(event.getCommandSource() instanceof Player player)) return;
CaptchaSession session = pending.get(player.getUniqueId());
if (session == null) return;
String cmd = event.getCommand();
if (!cmd.startsWith("verify ")) {
event.setResult(CommandExecuteEvent.CommandResult.denied());
player.sendMessage(Component.text("§c[Rampart] You must complete the CAPTCHA first. Type /verify <number>"));
}
}
@Subscribe
public void onDisconnect(DisconnectEvent event) {
pending.remove(event.getPlayer().getUniqueId());
}
private static class CaptchaSession {
final int token;
int attempts;
final long expiry;
CaptchaSession(int token, int attempts, long expiry) {
this.token = token;
this.attempts = attempts;
this.expiry = expiry;
}
}
}

View file

@ -9,6 +9,9 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class HmacCheckListener {
@ -16,6 +19,8 @@ public class HmacCheckListener {
private static final String HMAC_ALGO = "HmacSHA256";
private static final int HEX_SIG_LENGTH = 64;
private static final Map<UUID, Boolean> verifiedPlayers = new ConcurrentHashMap<>();
private final Logger logger;
private final byte[] secret;
@ -24,6 +29,11 @@ public class HmacCheckListener {
this.secret = secret.getBytes();
}
public static void markVerified(UUID uuid) {
verifiedPlayers.put(uuid, true);
PhysicsCheckListener.clearSuspicion(uuid);
}
@Subscribe
public void onLogin(LoginEvent event) {
var player = event.getPlayer();

View file

@ -0,0 +1,184 @@
package me.rampart.velocity;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import net.kyori.adventure.text.Component;
import org.slf4j.Logger;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class PhysicsCheckListener {
private static final double[] EXPECTED_Y = new double[128];
private static final int FALL_THRESHOLD = 3;
private static final int PROTOCOL_THRESHOLD = 3;
private static final int VEHICLE_THRESHOLD = 2;
private static final int PROTOCOL_HISTORY = 5;
private static final double MAX_BOAT_SPEED = 0.6;
private static final double MAX_MINECART_SPEED = 0.4;
static {
for (int t = 0; t < 128; t++) {
EXPECTED_Y[t] = 3.92 * (Math.pow(0.98, t) - 1);
}
}
private static final Map<UUID, PlayerTracker> tracked = new ConcurrentHashMap<>();
private final Logger logger;
private final ProxyServer server;
public PhysicsCheckListener(Logger logger, ProxyServer server) {
this.logger = logger;
this.server = server;
server.getScheduler().buildTask(this, this::tickAll)
.repeat(50, TimeUnit.MILLISECONDS)
.schedule();
}
public static void clearSuspicion(UUID uuid) {
PlayerTracker tracker = tracked.get(uuid);
if (tracker != null) {
tracker.suspicionScore = 0;
tracker.consecutiveFallViolations = 0;
tracker.protocolViolations = 0;
tracker.reVerificationTriggered = false;
}
}
@Subscribe
public void onLogin(LoginEvent event) {
Player player = event.getPlayer();
tracked.put(player.getUniqueId(), new PlayerTracker());
}
@Subscribe
public void onDisconnect(DisconnectEvent event) {
tracked.remove(event.getPlayer().getUniqueId());
}
@Subscribe
public void onPlayerSettings(PlayerSettingsChangedEvent event) {
PlayerTracker tracker = tracked.get(event.getPlayer().getUniqueId());
if (tracker == null) return;
tracker.virtualTick++;
tracker.lastEventTime = System.currentTimeMillis();
tracker.addProtocol("Settings");
}
@Subscribe
public void onPlayerChat(PlayerChatEvent event) {
PlayerTracker tracker = tracked.get(event.getPlayer().getUniqueId());
if (tracker == null) return;
tracker.virtualTick++;
tracker.lastEventTime = System.currentTimeMillis();
tracker.addProtocol("Chat");
}
@Subscribe
public void onTabComplete(TabCompleteEvent event) {
PlayerTracker tracker = tracked.get(event.getPlayer().getUniqueId());
if (tracker == null) return;
tracker.addProtocol("TabComplete");
}
@Subscribe
public void onResourcePackStatus(PlayerResourcePackStatusEvent event) {
PlayerTracker tracker = tracked.get(event.getPlayer().getUniqueId());
if (tracker == null) return;
tracker.addProtocol("ResourcePack");
}
private void tickAll() {
for (Map.Entry<UUID, PlayerTracker> entry : tracked.entrySet()) {
Player player = server.getPlayer(entry.getKey()).orElse(null);
if (player == null) continue;
PlayerTracker tracker = entry.getValue();
checkFalling(player, tracker);
checkProtocol(player, tracker);
}
}
private void checkFalling(Player player, PlayerTracker tracker) {
if (player.hasPermission("rampart.bypass.fly")) return;
if (tracker.virtualTick >= 128) return;
if (tracker.virtualTick < 0) return;
double expectedDelta = EXPECTED_Y[tracker.virtualTick];
double actualDelta = 0;
if (tracker.lastEventTime > 0) {
long dt = System.currentTimeMillis() - tracker.lastEventTime;
actualDelta = 3.92 * (Math.pow(0.98, Math.max(1, dt / 50.0)) - 1);
}
if (Math.abs(actualDelta - expectedDelta) > 0.001) {
tracker.consecutiveFallViolations++;
if (tracker.consecutiveFallViolations >= FALL_THRESHOLD) {
tracker.suspicionScore += FALL_THRESHOLD;
tracker.consecutiveFallViolations = 0;
logger.warn("Falling check triggered for {} (score={})",
player.getUsername(), tracker.suspicionScore);
if (tracker.suspicionScore >= FALL_THRESHOLD) {
triggerReVerify(player, tracker);
}
}
} else {
tracker.consecutiveFallViolations = 0;
}
}
private void checkProtocol(Player player, PlayerTracker tracker) {
if (tracker.protocolHistory.size() < PROTOCOL_HISTORY) return;
boolean unexpected = false;
String last = tracker.protocolHistory.getLast();
if (last.equals("Chat") || last.equals("TabComplete")) {
} else {
unexpected = true;
tracker.protocolViolations++;
if (tracker.protocolViolations >= PROTOCOL_THRESHOLD) {
tracker.suspicionScore += PROTOCOL_THRESHOLD;
tracker.protocolViolations = 0;
logger.warn("Protocol check triggered for {} (score={})",
player.getUsername(), tracker.suspicionScore);
if (tracker.suspicionScore >= PROTOCOL_THRESHOLD) {
triggerReVerify(player, tracker);
}
}
}
}
private void triggerReVerify(Player player, PlayerTracker tracker) {
if (tracker.reVerificationTriggered) return;
tracker.reVerificationTriggered = true;
player.disconnect(Component.text("Re-verification required. Please reconnect."));
logger.info("Triggered re-verification for {}", player.getUsername());
}
private static class PlayerTracker {
int virtualTick = 0;
long lastEventTime = 0;
int consecutiveFallViolations = 0;
int protocolViolations = 0;
int suspicionScore = 0;
boolean reVerificationTriggered = false;
Deque<String> protocolHistory = new ArrayDeque<>();
void addProtocol(String type) {
protocolHistory.addLast(type);
if (protocolHistory.size() > PROTOCOL_HISTORY) {
protocolHistory.removeFirst();
}
}
}
}

View file

@ -14,7 +14,7 @@ import java.util.List;
id = "rampart",
name = "Rampart",
version = "0.1.0",
description = "HMAC hostname verification + domain whitelist + Redis server registry for Rampart",
description = "HMAC hostname verification + domain whitelist + Redis server registry + load balancer",
authors = {"loki"}
)
public class RampartVelocity {
@ -25,20 +25,21 @@ public class RampartVelocity {
@Inject
public RampartVelocity(ProxyServer server, Logger logger) {
this.logger = logger;
EventManager em = server.getEventManager();
String secret = System.getenv("RAMPART_HMAC_SECRET");
List<String> allowed = loadDomainWhitelist();
if (!allowed.isEmpty()) {
logger.info("Domain whitelist: {} domains loaded", allowed.size());
server.getEventManager().register(this, new DomainCheckListener(logger, allowed));
em.register(this, new DomainCheckListener(logger, allowed));
} else {
logger.warn("RAMPART_ALLOWED_DOMAINS not set — domain check disabled");
}
if (secret != null && !secret.isEmpty()) {
logger.info("HMAC verification enabled");
server.getEventManager().register(this, new HmacCheckListener(logger, secret));
em.register(this, new HmacCheckListener(logger, secret));
} else {
logger.warn("RAMPART_HMAC_SECRET not set — HMAC verification disabled");
}
@ -50,7 +51,19 @@ public class RampartVelocity {
serverRegistry = new ServerRegistry(server, logger, redisUrl);
serverRegistry.startSync();
logger.info("Server registry sync started with Redis at {}", redisUrl);
ServerRouter router = new ServerRouter(serverRegistry, logger);
em.register(this, router);
PhysicsCheckListener physicsCheck = new PhysicsCheckListener(logger, server);
em.register(this, physicsCheck);
logger.info("Physics checks enabled");
CaptchaHandler captchaHandler = new CaptchaHandler(logger, server);
em.register(this, captchaHandler);
logger.info("CAPTCHA handler enabled");
logger.info("Server registry + load balancer started with Redis at {}", redisUrl);
}
private List<String> loadDomainWhitelist() {

View file

@ -12,6 +12,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@ -22,6 +23,7 @@ public class ServerRegistry {
private final Logger logger;
private final String redisUrl;
private final AtomicInteger counter = new AtomicInteger(0);
private final ConcurrentHashMap<String, Double> tpsCache = new ConcurrentHashMap<>();
private volatile List<RegisteredServer> cachedServers = new ArrayList<>();
public ServerRegistry(ProxyServer proxyServer, Logger logger, String redisUrl) {
@ -91,6 +93,8 @@ public class ServerRegistry {
if (port <= 0) continue;
String status = extractJsonString(json, "status");
if (!"online".equals(status)) continue;
double tps = extractJsonDouble(json, "tps");
tpsCache.put(name, tps);
servers.add(new ServerInfo(name, InetSocketAddress.createUnresolved(ip, port)));
} catch (Exception e) {
logger.warn("Failed to parse server data for key {}: {}", key, e.getMessage());
@ -113,6 +117,10 @@ public class ServerRegistry {
return cachedServers;
}
public double getServerTps(String name) {
return tpsCache.getOrDefault(name, 20.0);
}
private static String extractJsonString(String json, String key) {
String search = "\"" + key + "\":\"";
int start = json.indexOf(search);
@ -139,4 +147,21 @@ public class ServerRegistry {
return -1;
}
}
private static double extractJsonDouble(String json, String key) {
String search = "\"" + key + "\":";
int start = json.indexOf(search);
if (start < 0) return 20.0;
start += search.length();
int end = start;
while (end < json.length() && (Character.isDigit(json.charAt(end)) || json.charAt(end) == '.')) {
end++;
}
if (end == start) return 20.0;
try {
return Double.parseDouble(json.substring(start, end));
} catch (NumberFormatException e) {
return 20.0;
}
}
}

View file

@ -1,18 +1,49 @@
package me.rampart.velocity;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import org.slf4j.Logger;
import java.util.Optional;
public class ServerRouter {
private final ServerRegistry registry;
private static final double TPS_DEAD = 12.0;
public ServerRouter(ServerRegistry registry) {
private final ServerRegistry registry;
private final Logger logger;
public ServerRouter(ServerRegistry registry, Logger logger) {
this.registry = registry;
this.logger = logger;
}
@Subscribe
public void onServerPreConnect(ServerPreConnectEvent event) {
if (event.getResult().getServer().isPresent()) {
return;
}
String domain = event.getPlayer().getVirtualHost()
.map(vh -> vh.getHostString().split("\0")[0])
.orElse("");
Optional<RegisteredServer> target = routeServer(domain);
if (target.isPresent()) {
event.setResult(ServerPreConnectEvent.ServerResult.allowed(target.get()));
} else {
logger.warn("No available backend server for {} (domain: {})",
event.getPlayer().getRemoteAddress(), domain);
}
}
public Optional<RegisteredServer> routeServer(String domain) {
for (RegisteredServer server : registry.getCachedServers()) {
double tps = registry.getServerTps(server.getServerInfo().getName());
if (tps >= TPS_DEAD) {
return Optional.of(server);
}
}
return registry.getNextServer();
}
}