fix
This commit is contained in:
@@ -3,5 +3,6 @@
|
||||
"psk": "change-me-to-a-long-random-passphrase",
|
||||
"timestampWindowMs": 30000,
|
||||
"pendingTimeoutMs": 10000,
|
||||
"streamWindowBytes": 262144
|
||||
"streamWindowBytes": 262144,
|
||||
"sessionIdleTimeoutMs": 90000
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ public record Config(
|
||||
String psk,
|
||||
long timestampWindowMs,
|
||||
long pendingTimeoutMs,
|
||||
int streamWindowBytes
|
||||
int streamWindowBytes,
|
||||
long sessionIdleTimeoutMs
|
||||
) {
|
||||
public static Config load(Path file) throws Exception {
|
||||
JsonObject json = new JsonObject(Files.readString(file));
|
||||
@@ -35,6 +36,9 @@ public record Config(
|
||||
psk,
|
||||
json.getLong("timestampWindowMs", 30_000L),
|
||||
json.getLong("pendingTimeoutMs", 10_000L),
|
||||
window);
|
||||
window,
|
||||
// Comfortably above the client's default 20s ping interval;
|
||||
// 0 disables the watchdog.
|
||||
json.getLong("sessionIdleTimeoutMs", 90_000L));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,9 @@ public final class HubConnection {
|
||||
return;
|
||||
}
|
||||
peerWindow = Math.min(peerWindow, Protocol.MAX_STREAM_WINDOW);
|
||||
// Heartbeat is optional: accept it only when the client offered it, so
|
||||
// older clients keep working (they just lose silent-path detection).
|
||||
boolean heartbeat = (flags & Protocol.FLAG_WORKER_HEARTBEAT) != 0;
|
||||
|
||||
// REKEY = Rand || Timestamp(I64 big-endian). Magic is excluded.
|
||||
byte[] rekey = new byte[randLen + 8];
|
||||
@@ -179,9 +182,10 @@ public final class HubConnection {
|
||||
Crypto.decryptCipher(rekey, Crypto.DIR_C2S),
|
||||
Crypto.encryptCipher(rekey, Crypto.DIR_S2C));
|
||||
// Echo the accepted flags plus our own receive window.
|
||||
int accepted = Protocol.FLAG_STREAM_FC | (heartbeat ? Protocol.FLAG_WORKER_HEARTBEAT : 0);
|
||||
frames.send(new ProtoWriter()
|
||||
.u8(Protocol.CTL_SESSION_READY)
|
||||
.varInt(Protocol.FLAG_STREAM_FC)
|
||||
.varInt(accepted)
|
||||
.varInt(hub.config.streamWindowBytes())
|
||||
.toBytes());
|
||||
|
||||
@@ -189,16 +193,48 @@ public final class HubConnection {
|
||||
ControlSession session = new ControlSession(hub, frames, id);
|
||||
frames.setHandler(session::onFrame);
|
||||
closeCleanup = session::onClose;
|
||||
LOG.info("{} control session established", id);
|
||||
LOG.info("{} control session established (heartbeat {})", id, heartbeat);
|
||||
} else if (magic == Protocol.MAGIC_WORKER) {
|
||||
WorkerConn worker = new WorkerConn(hub, frames, id, peerWindow, hub.config.streamWindowBytes());
|
||||
frames.setHandler(worker::onFrame);
|
||||
closeCleanup = worker::onClose;
|
||||
LOG.info("{} worker conn established (peer window {})", id, peerWindow);
|
||||
LOG.info("{} worker conn established (peer window {}, heartbeat {})", id, peerWindow, heartbeat);
|
||||
} else {
|
||||
LOG.warn("{} bad magic {}; closing", id, magic);
|
||||
frames.close();
|
||||
return;
|
||||
}
|
||||
armIdleWatchdog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop an established redapricot session that has gone silent. Clients ping
|
||||
* both their control session and every worker conn, so silence means the
|
||||
* path is dead — without this the hub would keep a zombie control session
|
||||
* registered and keep routing players into it, and zombie worker conns would
|
||||
* hold player sockets open forever. Player connections are never subject to
|
||||
* this; only authenticated sessions are.
|
||||
*/
|
||||
private void armIdleWatchdog() {
|
||||
long idleMs = hub.config.sessionIdleTimeoutMs();
|
||||
if (idleMs <= 0) return;
|
||||
long timer = hub.vertx.setPeriodic(idleMs / 2, tid -> {
|
||||
if (frames.isClosed()) {
|
||||
hub.vertx.cancelTimer(tid);
|
||||
return;
|
||||
}
|
||||
long silent = System.currentTimeMillis() - frames.lastFrameAt();
|
||||
if (silent > idleMs) {
|
||||
LOG.warn("{} session silent for {}ms; closing", id, silent);
|
||||
hub.vertx.cancelTimer(tid);
|
||||
frames.close();
|
||||
}
|
||||
});
|
||||
Runnable inner = closeCleanup;
|
||||
closeCleanup = () -> {
|
||||
hub.vertx.cancelTimer(timer);
|
||||
inner.run();
|
||||
};
|
||||
}
|
||||
|
||||
// ---- player connection ----
|
||||
|
||||
@@ -24,6 +24,9 @@ public final class HubServer extends AbstractVerticle {
|
||||
.setHost(config.host())
|
||||
.setPort(config.port())
|
||||
.setTcpNoDelay(true)
|
||||
// Probe idle sockets so a peer that becomes unreachable is
|
||||
// eventually detected even when no frames are in flight.
|
||||
.setTcpKeepAlive(true)
|
||||
.setReuseAddress(true);
|
||||
|
||||
NetServer server = vertx.createNetServer(opts);
|
||||
|
||||
@@ -32,10 +32,17 @@ public final class Protocol {
|
||||
public static final int MUX_FIN = 0x02;
|
||||
public static final int MUX_RST = 0x03;
|
||||
public static final int MUX_WND = 0x04; // per-stream flow-control credit grant
|
||||
public static final int MUX_PING = 0x05; // liveness probe, StreamID 0
|
||||
public static final int MUX_PONG = 0x06; // liveness reply, echoes the nonce
|
||||
|
||||
/** Reserved stream id for connection-scoped mux frames (PING/PONG). Streams start at 1. */
|
||||
public static final int MUX_CTL_SID = 0;
|
||||
|
||||
// Session-establishment feature flags (trailing VarInt on the Rekey message,
|
||||
// echoed after the SessionReady type byte when accepted).
|
||||
public static final int FLAG_STREAM_FC = 0x01;
|
||||
/** Mux-level PING/PONG on worker conns, so a silently dropped path is detected. */
|
||||
public static final int FLAG_WORKER_HEARTBEAT = 0x02;
|
||||
|
||||
// Per-stream flow-control window bounds (bytes).
|
||||
public static final int DEFAULT_STREAM_WINDOW = 256 * 1024;
|
||||
|
||||
@@ -65,6 +65,8 @@ public final class WorkerConn {
|
||||
case Protocol.MUX_DATA -> handleData(sid, r.readBuffer(r.remaining()));
|
||||
case Protocol.MUX_WND -> handleWnd(sid, r.readVarInt());
|
||||
case Protocol.MUX_FIN, Protocol.MUX_RST -> closeStream(sid);
|
||||
case Protocol.MUX_PING -> sendPong(r.readI64());
|
||||
case Protocol.MUX_PONG -> { /* liveness only; arrival is what matters */ }
|
||||
case Protocol.FRAME_ERROR -> LOG.warn("worker {} error frame", id);
|
||||
default -> LOG.warn("worker {} unknown mux type {}", id, type);
|
||||
}
|
||||
@@ -217,6 +219,11 @@ public final class WorkerConn {
|
||||
frames.send(new ProtoWriter().u8(Protocol.MUX_WND).varInt(sid).varInt(delta).toBytes());
|
||||
}
|
||||
|
||||
/** Answer the client's liveness probe, echoing its nonce. */
|
||||
private void sendPong(long nonce) {
|
||||
frames.send(new ProtoWriter().u8(Protocol.MUX_PONG).varInt(Protocol.MUX_CTL_SID).i64(nonce).toBytes());
|
||||
}
|
||||
|
||||
public void onClose() {
|
||||
for (StreamState st : streams.values()) st.player.close();
|
||||
streams.clear();
|
||||
|
||||
@@ -23,6 +23,7 @@ public final class EncryptedFrames {
|
||||
private FrameHandler handler;
|
||||
private Buffer buf = Buffer.buffer();
|
||||
private boolean closed = false;
|
||||
private long lastFrameAt = System.currentTimeMillis();
|
||||
|
||||
public EncryptedFrames(NetSocket socket, Cipher in, Cipher out, FrameHandler handler) {
|
||||
this.socket = socket;
|
||||
@@ -71,6 +72,7 @@ public final class EncryptedFrames {
|
||||
byte[] pt = in.update(ct);
|
||||
if (pt == null) pt = new byte[0];
|
||||
buf = buf.getBuffer(hdr + payloadLen, buf.length());
|
||||
lastFrameAt = System.currentTimeMillis();
|
||||
FrameHandler h = handler;
|
||||
if (h != null) {
|
||||
try {
|
||||
@@ -96,6 +98,9 @@ public final class EncryptedFrames {
|
||||
|
||||
public boolean writeQueueFull() { return socket.writeQueueFull(); }
|
||||
|
||||
/** Wall-clock millis when the last complete frame was decoded; basis for idle detection. */
|
||||
public long lastFrameAt() { return lastFrameAt; }
|
||||
|
||||
public void close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
|
||||
@@ -77,7 +77,8 @@ class CryptoCodecTest {
|
||||
|
||||
/** A Hub whose event loop is never touched (register/match/normalize use no Vert.x state). */
|
||||
private static Hub testHub() {
|
||||
return new Hub(null, new Config("0.0.0.0", 25565, "test-psk", 30_000L, 10_000L, Protocol.DEFAULT_STREAM_WINDOW));
|
||||
return new Hub(null, new Config("0.0.0.0", 25565, "test-psk", 30_000L, 10_000L,
|
||||
Protocol.DEFAULT_STREAM_WINDOW, 90_000L));
|
||||
}
|
||||
|
||||
private static ControlSession testSession(Hub hub, String id) {
|
||||
|
||||
Reference in New Issue
Block a user