feat: velocity routing, server registry and RampartVelocity tests

This commit is contained in:
loki5512344 2026-08-03 10:54:28 +02:00
parent fa6de281fb
commit 29ce6fb8e9
Signed by: boba
GPG key ID: 253067914055423B
41 changed files with 1239 additions and 532 deletions

View file

@ -8,8 +8,6 @@ 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) {
@ -21,28 +19,10 @@ public class HmacLoginListener implements Listener {
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)) {
if (!plugin.verifyHostname(raw)) {
plugin.getLogger().warning("HMAC verification failed for " + event.getAddress());
event.disallow(PlayerLoginEvent.Result.KICK_OTHER,
Component.text("Connection rejected: invalid signature"));

View file

@ -2,14 +2,25 @@ package me.rampart.paper;
import org.bukkit.plugin.java.JavaPlugin;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public class RampartPaper extends JavaPlugin {
static final String SHIELD_SEPARATOR = "\0shield\0";
static final String HMAC_ALGO = "HmacSHA256";
static final int HEX_SIG_LENGTH = 64;
private ShieldAgent shieldAgent;
private long rotationSecs = 3600;
private long ttlSecs = 60;
@Override
public void onEnable() {
rotationSecs = envLong("RAMPART_HMAC_ROTATION_SECS", 3600);
ttlSecs = envLong("RAMPART_HMAC_TTL_SECS", 60);
String secret = System.getenv("RAMPART_HMAC_SECRET");
if (secret == null || secret.isEmpty()) {
getLogger().warning("RAMPART_HMAC_SECRET not set — HMAC verification disabled");
@ -35,22 +46,66 @@ public class RampartPaper extends JavaPlugin {
}
}
String hmacHex(String data, byte[] secret) {
boolean verifyHostname(String raw) {
String secretEnv = System.getenv("RAMPART_HMAC_SECRET");
if (secretEnv == null || secretEnv.isEmpty()) return false;
return verifyHostname(raw, secretEnv.getBytes(StandardCharsets.UTF_8), rotationSecs, ttlSecs);
}
private boolean verifyHostname(String raw, byte[] secret, long rotation, long ttl) {
int sepIdx = raw.indexOf(SHIELD_SEPARATOR);
if (sepIdx < 0) return false;
String domain = raw.substring(0, sepIdx);
String rest = raw.substring(sepIdx + SHIELD_SEPARATOR.length());
int tsSep = rest.indexOf('\0');
if (tsSep < 0) return false;
String tsStr = rest.substring(0, tsSep);
String sig = rest.substring(tsSep + 1);
if (sig.length() != HEX_SIG_LENGTH || !sig.matches("[0-9a-f]+")) return false;
long ts;
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));
ts = Long.parseLong(tsStr);
} catch (NumberFormatException e) {
return false;
}
long now = System.currentTimeMillis() / 1000;
if (now < ts || now - ts > ttl) return false;
long tsBucket = ts / rotation;
for (long bucket : new long[]{tsBucket, tsBucket - 1}) {
byte[] derivedKey = hmacBytes(secret, ("rampart-key-" + bucket).getBytes(StandardCharsets.UTF_8));
if (derivedKey == null) continue;
String expected = toHex(hmacBytes(derivedKey, (domain + "|" + ts).getBytes(StandardCharsets.UTF_8)));
if (expected != null && constantTimeEquals(sig, expected)) {
return true;
}
return sb.toString();
}
return false;
}
private byte[] hmacBytes(byte[] key, byte[] data) {
try {
Mac mac = Mac.getInstance(HMAC_ALGO);
mac.init(new SecretKeySpec(key, HMAC_ALGO));
return mac.doFinal(data);
} catch (Exception e) {
getLogger().severe("HMAC error: " + e.getMessage());
return null;
}
}
private static String toHex(byte[] raw) {
StringBuilder sb = new StringBuilder(raw.length * 2);
for (byte b : raw) {
sb.append(String.format("%02x", b & 0xFF));
}
return sb.toString();
}
boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) return false;
int result = 0;
@ -59,4 +114,14 @@ public class RampartPaper extends JavaPlugin {
}
return result == 0;
}
private static long envLong(String name, long def) {
String value = System.getenv(name);
if (value == null || value.isEmpty()) return def;
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return def;
}
}
}

View file

@ -14,6 +14,7 @@ public class ShieldAgent {
private final RampartPaper plugin;
private final Jedis jedis;
private final String serverName;
private final String serverDomain;
private final String serverIp;
private final int serverPort;
private BukkitRunnable task;
@ -35,6 +36,9 @@ public class ShieldAgent {
}
this.serverName = name;
String domain = System.getenv("RAMPART_SERVER_DOMAIN");
this.serverDomain = (domain == null) ? "" : domain;
String ip = System.getenv("RAMPART_SERVER_IP");
if (ip == null || ip.isEmpty()) {
try {
@ -108,7 +112,8 @@ public class ShieldAgent {
}
private String buildJson(String status, int online, int maxPlayers, double tps) {
return "{\"name\":\"" + serverName + "\",\"type\":\"paper\",\"ip\":\"" + serverIp
return "{\"name\":\"" + serverName + "\",\"type\":\"paper\",\"domain\":\"" + serverDomain
+ "\",\"ip\":\"" + serverIp
+ "\",\"port\":" + serverPort + ",\"status\":\"" + status
+ "\",\"online\":" + online + ",\"max_players\":" + maxPlayers
+ ",\"tps\":" + tps