diff --git a/scripts/cloud_bench_orchestrate.mjs b/scripts/cloud_bench_orchestrate.mjs index db40af1..676b824 100755 --- a/scripts/cloud_bench_orchestrate.mjs +++ b/scripts/cloud_bench_orchestrate.mjs @@ -135,6 +135,7 @@ Notes: - Caches built nostr-bench at _build/bench/nostr-bench and reuses it when valid. - Auto-tunes Postgres/Redis/app pool sizing from server RAM + CPU for DB-backed targets. - Randomizes target order per run and wipes persisted target data directories on each start. + - Creates a Hetzner Cloud firewall restricting inbound access to benchmark ports from known IPs only. - Handles Ctrl-C / SIGTERM with best-effort cloud cleanup. - Tries nix .#nostrBenchStaticX86_64Musl first; falls back to docker-built portable nostr-bench. - If --parrhesia-image is omitted, requires nix locally. @@ -1396,6 +1397,8 @@ async function main() { const createdServers = []; let sshKeyCreated = false; + let firewallName = null; + let firewallCreated = false; let cleanupPromise = null; const cleanup = async () => { @@ -1424,6 +1427,17 @@ async function main() { ); } + if (firewallCreated) { + console.log("[cleanup] deleting firewall..."); + await runCommand("hcloud", ["firewall", "delete", firewallName]) + .then(() => { + console.log(`[cleanup] deleted firewall: ${firewallName}`); + }) + .catch((error) => { + console.warn(`[cleanup] failed to delete firewall ${firewallName}: ${error.message || error}`); + }); + } + if (sshKeyCreated) { console.log("[cleanup] deleting ssh key..."); await runCommand("hcloud", ["ssh-key", "delete", keyName]) @@ -1530,6 +1544,42 @@ async function main() { ...clientInfos.map((client) => waitForSsh(client.ip, keyPath)), ]); + // Detect orchestrator public IP from the server's perspective. + const orchestratorIp = ( + await sshExec(serverIp, keyPath, "echo $SSH_CLIENT") + ).stdout.trim().split(/\s+/)[0]; + + // Create a firewall restricting inbound access to known benchmark IPs only. + firewallName = `${runId}-fw`; + const allBenchIps = [orchestratorIp, serverIp, ...clientInfos.map((c) => c.ip)]; + const sourceIps = [...new Set(allBenchIps)].map((ip) => `${ip}/32`); + + const firewallRules = [ + { direction: "in", protocol: "tcp", port: "22", source_ips: sourceIps, description: "SSH" }, + { direction: "in", protocol: "tcp", port: "3355", source_ips: sourceIps, description: "Haven" }, + { direction: "in", protocol: "tcp", port: "4413", source_ips: sourceIps, description: "Parrhesia" }, + { direction: "in", protocol: "tcp", port: "7777", source_ips: sourceIps, description: "strfry" }, + { direction: "in", protocol: "tcp", port: "8008", source_ips: sourceIps, description: "Nostream" }, + { direction: "in", protocol: "tcp", port: "8080", source_ips: sourceIps, description: "nostr-rs-relay" }, + { direction: "in", protocol: "tcp", port: "9090", source_ips: sourceIps, description: "Prometheus" }, + { direction: "in", protocol: "tcp", port: "9100", source_ips: sourceIps, description: "node_exporter" }, + { direction: "in", protocol: "icmp", source_ips: ["0.0.0.0/0", "::/0"], description: "ICMP" }, + ]; + + const rulesPath = path.join(tmpDir, "firewall-rules.json"); + fs.writeFileSync(rulesPath, JSON.stringify(firewallRules)); + + await runCommand("hcloud", ["firewall", "create", "--name", firewallName, "--rules-file", rulesPath]); + firewallCreated = true; + + for (const name of createdServers) { + await runCommand("hcloud", [ + "firewall", "apply-to-resource", firewallName, + "--type", "server", "--server", name, + ]); + } + console.log(`[firewall] ${firewallName} applied (sources: ${sourceIps.join(", ")})`); + console.log("[phase] install runtime dependencies on server node"); const serverInstallCmd = [ "set -euo pipefail",