feat: velocity routing, server registry and RampartVelocity tests
This commit is contained in:
parent
fa6de281fb
commit
29ce6fb8e9
41 changed files with 1239 additions and 532 deletions
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue