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:
commit
cf9608ce5d
159 changed files with 15341 additions and 0 deletions
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
117
plugins/paper/src/main/java/me/rampart/paper/ShieldAgent.java
Normal file
117
plugins/paper/src/main/java/me/rampart/paper/ShieldAgent.java
Normal 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 + "}";
|
||||
}
|
||||
}
|
||||
6
plugins/paper/src/main/resources/plugin.yml
Normal file
6
plugins/paper/src/main/resources/plugin.yml
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue