Initial commit: Rampart v0.2.0

Multi-layer DDoS protection for Minecraft servers.

- rampart-core: Edge node with XDP/eBPF + Rust L7 filtering
- rampart-manager: REST API with JWT auth, Redis sync
- rampart-cli: CLI tool for operators
- velocity-plugin: Domain check, HMAC verify, server registry, load balancer
- paper-plugin: Auto-registration, heartbeat, HMAC verify
- dashboard: React + Vite web UI for management
This commit is contained in:
loki5512344 2026-07-20 20:53:32 +02:00
commit cf9608ce5d
Signed by: boba
GPG key ID: 253067914055423B
159 changed files with 15341 additions and 0 deletions

View file

@ -0,0 +1,51 @@
package me.rampart.paper;
import net.kyori.adventure.text.Component;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
public class HmacLoginListener implements Listener {
private static final int HEX_SIG_LENGTH = 64;
private final RampartPaper plugin;
public HmacLoginListener(RampartPaper plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerLogin(PlayerLoginEvent event) {
String secretEnv = System.getenv("RAMPART_HMAC_SECRET");
if (secretEnv == null || secretEnv.isEmpty()) return;
byte[] secret = secretEnv.getBytes();
String raw = event.getHostname();
if (raw == null || raw.isEmpty()) return;
int sepIdx = raw.indexOf(RampartPaper.SHIELD_SEPARATOR);
if (sepIdx < 0) {
return;
}
String domain = raw.substring(0, sepIdx);
String sig = raw.substring(sepIdx + RampartPaper.SHIELD_SEPARATOR.length());
if (sig.length() != HEX_SIG_LENGTH) {
plugin.getLogger().warning("Invalid sig length from " + event.getAddress() +
": got " + sig.length() + ", expected " + HEX_SIG_LENGTH);
event.disallow(PlayerLoginEvent.Result.KICK_OTHER,
Component.text("Connection rejected: invalid signature"));
return;
}
String expected = plugin.hmacHex(domain, secret);
if (expected == null || !plugin.constantTimeEquals(sig, expected)) {
plugin.getLogger().warning("HMAC verification failed for " + event.getAddress());
event.disallow(PlayerLoginEvent.Result.KICK_OTHER,
Component.text("Connection rejected: invalid signature"));
}
}
}

View file

@ -0,0 +1,62 @@
package me.rampart.paper;
import org.bukkit.plugin.java.JavaPlugin;
public class RampartPaper extends JavaPlugin {
static final String SHIELD_SEPARATOR = "\0shield\0";
private ShieldAgent shieldAgent;
@Override
public void onEnable() {
String secret = System.getenv("RAMPART_HMAC_SECRET");
if (secret == null || secret.isEmpty()) {
getLogger().warning("RAMPART_HMAC_SECRET not set — HMAC verification disabled");
} else {
getLogger().info("Rampart HMAC verification enabled");
}
getServer().getPluginManager().registerEvents(new HmacLoginListener(this), this);
try {
shieldAgent = new ShieldAgent(this);
shieldAgent.start();
getLogger().info("ShieldAgent started");
} catch (Exception e) {
getLogger().severe("Failed to start ShieldAgent: " + e.getMessage());
}
}
@Override
public void onDisable() {
if (shieldAgent != null) {
shieldAgent.shutdown();
getLogger().info("ShieldAgent shut down");
}
}
String hmacHex(String data, byte[] secret) {
try {
var mac = javax.crypto.Mac.getInstance("HmacSHA256");
mac.init(new javax.crypto.spec.SecretKeySpec(secret, "HmacSHA256"));
byte[] raw = mac.doFinal(data.getBytes());
StringBuilder sb = new StringBuilder(raw.length * 2);
for (byte b : raw) {
sb.append(String.format("%02x", b & 0xFF));
}
return sb.toString();
} catch (Exception e) {
getLogger().severe("HMAC error: " + e.getMessage());
return null;
}
}
boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) return false;
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
}

View file

@ -0,0 +1,117 @@
package me.rampart.paper;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitRunnable;
import redis.clients.jedis.Jedis;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.concurrent.ThreadLocalRandom;
public class ShieldAgent {
private final RampartPaper plugin;
private final Jedis jedis;
private final String serverName;
private final String serverIp;
private final int serverPort;
private BukkitRunnable task;
public ShieldAgent(RampartPaper plugin) {
this.plugin = plugin;
String redisUrl = System.getenv("RAMPART_REDIS_URL");
if (redisUrl == null || redisUrl.isEmpty()) {
redisUrl = "redis://127.0.0.1:6379/0";
}
this.jedis = new Jedis(URI.create(redisUrl));
String name = System.getenv("RAMPART_SERVER_NAME");
if (name == null || name.isEmpty()) {
int rand = ThreadLocalRandom.current().nextInt(0x10000);
name = "paper-" + String.format("%04x", rand);
}
this.serverName = name;
String ip = System.getenv("RAMPART_SERVER_IP");
if (ip == null || ip.isEmpty()) {
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
ip = "127.0.0.1";
}
}
this.serverIp = ip;
this.serverPort = plugin.getServer().getPort();
}
public void start() {
register();
task = new BukkitRunnable() {
@Override
public void run() {
heartbeat();
}
};
task.runTaskTimer(plugin, 0L, 20L);
}
public void shutdown() {
if (task != null) {
task.cancel();
}
try {
setOffline();
} finally {
jedis.close();
}
}
private void register() {
try {
String json = buildJson("online", plugin.getServer().getOnlinePlayers().size(),
plugin.getServer().getMaxPlayers(), Bukkit.getTPS()[0]);
jedis.set("rampart:servers:" + serverName, json);
plugin.getLogger().info("Registered server " + serverName + " in Redis at " + serverIp + ":" + serverPort);
} catch (Exception e) {
plugin.getLogger().severe("Failed to register in Redis: " + e.getMessage());
}
}
private void heartbeat() {
try {
int online = plugin.getServer().getOnlinePlayers().size();
int maxPlayers = plugin.getServer().getMaxPlayers();
double tps = Bukkit.getTPS()[0];
String json = buildJson("online", online, maxPlayers, tps);
jedis.set("rampart:servers:" + serverName, json);
} catch (Exception e) {
plugin.getLogger().severe("Heartbeat error: " + e.getMessage());
}
}
private void setOffline() {
try {
int online = plugin.getServer().getOnlinePlayers().size();
int maxPlayers = plugin.getServer().getMaxPlayers();
double tps = Bukkit.getTPS()[0];
String json = buildJson("offline", online, maxPlayers, tps);
jedis.set("rampart:servers:" + serverName, json);
plugin.getLogger().info("Server " + serverName + " marked offline in Redis");
} catch (Exception e) {
plugin.getLogger().severe("Failed to mark offline in Redis: " + e.getMessage());
}
}
private String buildJson(String status, int online, int maxPlayers, double tps) {
return "{\"name\":\"" + serverName + "\",\"type\":\"paper\",\"ip\":\"" + serverIp
+ "\",\"port\":" + serverPort + ",\"status\":\"" + status
+ "\",\"online\":" + online + ",\"max_players\":" + maxPlayers
+ ",\"tps\":" + tps
+ ",\"last_heartbeat\":" + System.currentTimeMillis() / 1000 + "}";
}
}

View file

@ -0,0 +1,6 @@
name: Rampart
version: '0.1.0'
main: me.rampart.paper.RampartPaper
api-version: '1.21'
author: loki
description: HMAC hostname verification for Rampart