java-topology/tools/mc-bench/raw-connect.mjs
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

193 lines
7.7 KiB
JavaScript

/**
* raw-connect.mjs — raw Minecraft protocol handshake for any server version.
*
* Performs the LOGIN handshake only (does not need minecraft-data for the version).
* Measures connection accept latency and capacity: how many simultaneous TCP
* connections the server accepts before refusing.
*
* Protocol: Minecraft Handshake → Login Start → (server login success or kick)
* Works with any version since the handshake packet structure is stable.
*
* Usage: node raw-connect.mjs <host> <port> <count>
*/
import net from 'net';
const HOST = process.argv[2] || 'localhost';
const PORT = parseInt(process.argv[3] || '25565');
const COUNT = parseInt(process.argv[4] || '20');
// ── Minecraft VarInt encoding ─────────────────────────────────────────────────
function writeVarInt(val) {
const bytes = [];
do {
let b = val & 0x7f;
val >>>= 7;
if (val !== 0) b |= 0x80;
bytes.push(b);
} while (val !== 0);
return Buffer.from(bytes);
}
function writeString(str) {
const strBuf = Buffer.from(str, 'utf8');
return Buffer.concat([writeVarInt(strBuf.length), strBuf]);
}
function packet(id, ...bufs) {
const payload = Buffer.concat([writeVarInt(id), ...bufs]);
return Buffer.concat([writeVarInt(payload.length), payload]);
}
// ── Handshake packet (ID 0x00, state HANDSHAKING) ────────────────────────────
// Fields: protocol_version (VarInt), server_address (string), server_port (u16), next_state (VarInt=2 login)
function handshakePacket(protocolVersion, host, port) {
return packet(0x00,
writeVarInt(protocolVersion), // protocol version
writeString(host),
Buffer.from([port >> 8, port & 0xff]),
writeVarInt(2) // next state: LOGIN
);
}
// ── Login Start packet (ID 0x00, state LOGIN) ─────────────────────────────────
// Fields: name (string), uuid (optional)
function loginStartPacket(name) {
return packet(0x00, writeString(name));
}
// ── Connect one bot ───────────────────────────────────────────────────────────
async function connectBot(index, protocolVersion) {
return new Promise((resolve) => {
const name = `bench_${String(index).padStart(3, '0')}`;
const t0 = Date.now();
const sock = new net.Socket();
let done = false;
const finish = (status, detail) => {
if (done) return;
done = true;
const ms = Date.now() - t0;
sock.destroy();
resolve({ index, name, status, detail, ms });
};
sock.setTimeout(15000);
sock.on('timeout', () => finish('timeout', 'connection timed out'));
sock.on('error', (e) => finish('error', e.message));
sock.connect(PORT, HOST, () => {
// Connected — send handshake then login start
sock.write(handshakePacket(protocolVersion, HOST, PORT));
sock.write(loginStartPacket(name));
});
sock.on('data', (buf) => {
// We just need to know the server responded (any response = accepted)
// Response could be: Login Success (0x02), Login Disconnect (0x00), Set Compression (0x03)
if (buf.length > 0) {
// Check first packet byte after length VarInt: try to identify packet type
const packetByte = buf[buf.length > 2 ? 2 : 1];
let statusStr = 'connected';
if (packetByte === 0x00) statusStr = 'kicked'; // Disconnect
else if (packetByte === 0x02) statusStr = 'accepted'; // Login Success
else if (packetByte === 0x03) statusStr = 'compress'; // Set Compression (pre-auth)
finish(statusStr, `${buf.length} bytes`);
}
});
});
}
// ── Probe server for protocol version ────────────────────────────────────────
async function probeVersion() {
return new Promise((resolve) => {
const sock = new net.Socket();
sock.setTimeout(5000);
sock.on('error', () => resolve(0));
sock.on('timeout', () => { sock.destroy(); resolve(0); });
sock.connect(PORT, HOST, () => {
// Status handshake: next_state=1 (STATUS)
const handshake = packet(0x00,
writeVarInt(0), // version 0 = unknown
writeString(HOST),
Buffer.from([PORT >> 8, PORT & 0xff]),
writeVarInt(1) // STATUS
);
const statusReq = packet(0x00); // Status Request
sock.write(Buffer.concat([handshake, statusReq]));
});
sock.on('data', (buf) => {
// Try to parse JSON from status response
try {
const str = buf.toString();
const jsonStart = str.indexOf('{');
const jsonEnd = str.lastIndexOf('}');
if (jsonStart >= 0 && jsonEnd > jsonStart) {
const json = JSON.parse(str.slice(jsonStart, jsonEnd + 1));
const protocol = json?.version?.protocol || 0;
const versionName = json?.version?.name || 'unknown';
resolve({ protocol, name: versionName, players: json?.players });
}
} catch (_) {}
sock.destroy();
resolve(0);
});
});
}
// ── Main ─────────────────────────────────────────────────────────────────────
console.log(`=== Minecraft Raw Connection Benchmark ===`);
console.log(` server: ${HOST}:${PORT}`);
console.log(` bots: ${COUNT}`);
// Probe server version
const serverInfo = await probeVersion();
const protocolVersion = typeof serverInfo === 'object' ? serverInfo.protocol : 0;
if (typeof serverInfo === 'object') {
console.log(` server version: ${serverInfo.name} (protocol ${protocolVersion})`);
if (serverInfo.players) {
console.log(` online: ${serverInfo.players.online}/${serverInfo.players.max}`);
}
}
console.log(``);
if (!protocolVersion) {
console.log('Could not probe server. Is it running?');
process.exit(1);
}
const wallStart = Date.now();
const results = [];
// Spawn all connections with stagger
for (let i = 0; i < COUNT; i++) {
await new Promise(r => setTimeout(r, 100));
results.push(connectBot(i, protocolVersion));
process.stdout.write(`\r connecting ${i + 1}/${COUNT}...`);
}
console.log('');
const all = await Promise.all(results);
const wallMs = Date.now() - wallStart;
// Analyse
const accepted = all.filter(r => ['accepted','compress','connected','kicked'].includes(r.status));
const errors = all.filter(r => r.status === 'error' || r.status === 'timeout');
const latencies = accepted.map(r => r.ms).sort((a,b) => a-b);
function pct(arr, p) {
const i = Math.ceil(p/100*arr.length)-1;
return arr[Math.max(0, Math.min(arr.length-1, i))];
}
console.log(`=== Results ===`);
console.log(` wall time: ${(wallMs/1000).toFixed(1)} s`);
console.log(` accepted: ${accepted.length}/${COUNT}`);
console.log(` errors: ${errors.length}`);
if (latencies.length > 0) {
console.log(` handshake latency:`);
console.log(` p50: ${pct(latencies, 50)} ms`);
console.log(` p95: ${pct(latencies, 95)} ms`);
console.log(` max: ${latencies[latencies.length-1]} ms`);
}
console.log(``);