#!/usr/bin/env python3 """Disposable, loopback-only fixtures for testing actual RustProxy processes. Runtime databases and credentials live in TemporaryDirectory, never in results. No pre-existing SQLite database, fixed port, or historical /mnt/data path is used. """ from __future__ import annotations import copy import gzip import hashlib from http.client import HTTPConnection, HTTPException import json import os from pathlib import Path import pwd import secrets import shutil import signal import socket import subprocess import tempfile import time def sha256(path: Path) -> str: result = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): result.update(block) return result.hexdigest() def write_json(path: Path, value) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n") def seal_results(directory: Path) -> None: """Checksums cover every regular evidence file, except the checksum manifest.""" paths = sorted(p for p in directory.rglob("*") if p.is_file() and p.name != "SHA256SUMS") (directory / "SHA256SUMS").write_text("".join( f"{sha256(path)} {path.relative_to(directory)}\n" for path in paths)) def http(port: int, path="/small.html", method="GET", headers=None, body=None, timeout=10, source_address=None): connection = HTTPConnection("127.0.0.1", port, timeout=timeout, source_address=source_address) try: connection.request(method, path, body, headers or {}) response = connection.getresponse() payload = response.read() return response.status, {k.lower(): v for k, v in response.getheaders()}, payload finally: connection.close() def free_port() -> int: with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) return sock.getsockname()[1] def wait_ready(port: int, process: subprocess.Popen, timeout=30): deadline = time.monotonic() + timeout while time.monotonic() < deadline: if process.poll() is not None: raise RuntimeError(f"process {process.pid} exited with {process.returncode}") try: http(port, "/", timeout=.5) return except (OSError, HTTPException): time.sleep(.05) raise RuntimeError(f"loopback listener {port} did not start") class Fixture: def __init__(self, results: Path, workers=4, temp_parent: Path | None = None, server_cpus=None, origin_cpus=None, origin_workers=None): self.results = results.resolve() self.results.mkdir(parents=True, exist_ok=True) # Keep live state outside the evidence tree. Even an interrupted run or # workspace synchronization must never put databases/keys in results. self.temp = tempfile.TemporaryDirectory(prefix="rustproxy-perf-", dir=temp_parent) self.root = Path(self.temp.name) self.root.chmod(0o755) # nginx workers must traverse the public fixture root self.site = self.root / "site" self.site.mkdir(mode=0o755) self.workers = workers self.server_cpus = server_cpus self.origin_cpus = origin_cpus self.origin_workers = origin_workers or workers self.children = [] self.handles = [] self.ports = set() self.origin_port = self.port() self.make_site() def port(self): while True: port = free_port() if port not in self.ports: self.ports.add(port) return port def make_site(self): # Exact, reproducible payload sizes match the original report's cases. small = b"RustProxy benchmark " small += b"x" * (1040 - len(small) - len(b"")) small += b"" files = {"small.html": small, "large.bin": bytes(range(256)) * 256, "huge.bin": bytes(range(256)) * 4096, "empty.txt": b"", "app.json": (b'{\n "message": "stable fixture",\n "values": [1, 2, 3]\n}\n'), "app.css": b"/* fixture comment */\nbody { color: red; padding: 0px; }\n", "compress.txt": (b"compressible RustProxy text fixture. " * 4096), "download.txt": b"download fixture\n"} for name, body in files.items(): (self.site / name).write_bytes(body) (self.site / "small.html.gz").write_bytes(gzip.compress(small, mtime=0)) (self.site / "directory").mkdir() (self.site / "directory" / "index.html").write_bytes(b"directory index\n") (self.site / "listing").mkdir() (self.site / "listing" / "visible.txt").write_bytes(b"listing fixture\n") (self.site / "inside-link.txt").symlink_to(self.site / "download.txt") self.outside = self.root / "outside-secret.txt" self.outside.write_bytes(b"MUST-NOT-BE-SERVED-OUTSIDE-ROOT\n") (self.site / "escape-link.txt").symlink_to(self.outside) write_json(self.results / "payloads.json", { name: {"bytes": len(body), "sha256": hashlib.sha256(body).hexdigest()} for name, body in files.items()}) def spawn(self, argv, name, env=None, affinity=None): log = (self.results / f"{name}.log").open("w") self.handles.append(log) if affinity: argv = ["taskset", "-c", affinity, *argv] child = subprocess.Popen([str(x) for x in argv], stdout=log, stderr=subprocess.STDOUT, env=env, start_new_session=True) self.children.append(child) return child @staticmethod def stop(process): if process.poll() is None: os.killpg(process.pid, signal.SIGTERM) try: process.wait(timeout=12) except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL) process.wait(timeout=3) def close(self): for process in reversed(self.children): self.stop(process) for handle in self.handles: handle.close() self.temp.cleanup() write_json(self.results / "fixture-cleanup.json", { "temporary_state_removed": not self.root.exists(), "state_was_outside_results": not self.root.is_relative_to(self.results)}) if self.root.exists(): raise RuntimeError("temporary runtime state was not removed; do not package the fixture directory") def __enter__(self): return self def __exit__(self, *_): self.close() def base_config(self, reverse_port, static_port): endpoints = [] for label, port in [("reverse", reverse_port), ("static", static_port)]: endpoints.append({"id": label, "name": label, "scope": "internal", "enabled": True, "binding": { "address": f"127.0.0.1:{port}", "tls": False, "http2": False, "http3": None}}) return { "cache": {"enabled": False}, "geoip_updates": {"enabled": False}, "listeners": {"endpoints": endpoints}, "web_services": [{"id": "fixture-static", "name": "Fixture static", "document_root": str(self.site), "index": ["index.html", "small.html"], "directory_listing": False}], "vhosts": [{"id": "reverse", "frontend": { "hosts": ["127.0.0.1", "localhost", "fixture.test"], "listener_ids": ["reverse"]}, "backends": [{"id": "origin", "url": f"http://127.0.0.1:{self.origin_port}", "allow_private": True, "preserve_host": False}], "load_balancing": {"health_check": {"mode": "off"}}, "routes": []}, {"id": "static", "frontend": { "hosts": ["127.0.0.1", "localhost", "fixture.test"], "listener_ids": ["static"]}, "source": {"type": "web_server", "web_service_id": "fixture-static"}, "routes": []}] } def rust(self, binary: Path, label: str, max_idle=None): return RustInstance(self, binary.resolve(), label, max_idle=max_idle) def nginx(self, executable: Path, frontend=True): """Use a separate nginx master for the shared origin and comparator.""" executable = executable.resolve() result = subprocess.run([str(executable), "-V"], capture_output=True, text=True, timeout=10) (self.results / "nginx-version.txt").write_text(result.stdout + result.stderr) origin_conf = self.nginx_config("nginx-origin", f""" server {{ listen 127.0.0.1:{self.origin_port}; server_name _; root "{self.site}"; location / {{ add_header Cache-Control "public, max-age=60"; }} }} """, workers=self.origin_workers) origin = self.spawn([executable, "-p", self.root, "-c", origin_conf], "nginx-origin", affinity=self.origin_cpus) wait_ready(self.origin_port, origin) result = {"origin": origin, "origin_port": self.origin_port} if frontend: reverse_port, static_port = self.port(), self.port() conf = self.nginx_config("nginx-frontend", f""" upstream fixture_origin {{ server 127.0.0.1:{self.origin_port}; keepalive 64; }} server {{ listen 127.0.0.1:{reverse_port}; server_name _; location / {{ proxy_pass http://fixture_origin; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; }} }} server {{ listen 127.0.0.1:{static_port}; server_name _; root "{self.site}"; }} """) process = self.spawn([executable, "-p", self.root, "-c", conf], "nginx-frontend", affinity=self.server_cpus) wait_ready(reverse_port, process) wait_ready(static_port, process) result.update({"process": process, "reverse_port": reverse_port, "static_port": static_port}) return result def nginx_config(self, name, servers, workers=None): path = self.root / f"{name}.conf" text = f"""daemon off; master_process on; user {pwd.getpwuid(os.geteuid()).pw_name}; worker_processes {workers or self.workers}; pid {self.root}/{name}.pid; error_log stderr warn; events {{ worker_connections 8192; }} http {{ access_log off; sendfile on; tcp_nopush on; keepalive_timeout 65; keepalive_requests 1000000; default_type application/octet-stream; types {{ text/html html; text/css css; application/json json; text/plain txt; }} client_body_temp_path {self.root}/{name}-body; proxy_temp_path {self.root}/{name}-proxy; {servers} }} """ path.write_text(text) (self.results / path.name).write_text(text) return path class RustInstance: def __init__(self, fixture, binary, label, max_idle=None): self.fixture, self.binary, self.label = fixture, binary, label if not os.access(binary, os.X_OK): target = fixture.root / f"{label}-rust-proxy" shutil.copyfile(binary, target) target.chmod(0o700) self.binary = target self.admin_port, self.reverse_port, self.static_port = (fixture.port() for _ in range(3)) self.data = fixture.root / f"{label}-data" self.base = fixture.base_config(self.reverse_port, self.static_port) if max_idle is not None: self.base["vhosts"][0]["load_balancing"]["keepalive"] = {"max_idle_per_host": max_idle} self.password = "Fixture!" + secrets.token_hex(20) self.username = "performance" self.env = os.environ.copy() self.env["TOKIO_WORKER_THREADS"] = str(fixture.workers) self.env["RUST_PROXY_PROXY_LISTEN"] = f"127.0.0.1:{self.reverse_port}" self.env["RUST_PROXY_UPDATE_CHECK_INTERVAL_SECS"] = "0" self.env.pop("RUST_PROXY_TLS_LISTEN", None) self.applies = [] self.cli("admin", "init", "--username", self.username, "--password-stdin", input=self.password + "\n") self.start("bootstrap") self.apply(self.base, "base") fixture.stop(self.process) self.start("runtime") wait_ready(self.static_port, self.process) version = self.cli("--version").stdout.strip() write_json(fixture.results / f"{label}-binary.json", { "label": label, "path": str(binary), "sha256": sha256(binary), "version": version, "worker_threads": fixture.workers, "pid": self.process.pid, "protocol": "HTTP/1.1 plaintext loopback", "cpu_affinity": fixture.server_cpus, "max_idle_per_host_override": max_idle}) def cli(self, *args, input=None, check=True): result = subprocess.run([str(self.binary), "--data-dir", str(self.data), "--admin-listen", f"127.0.0.1:{self.admin_port}", *args], input=input, text=True, capture_output=True, env=self.env, timeout=40) if check and result.returncode: raise RuntimeError(f"{self.label} CLI {' '.join(args[:2])}: " f"{result.stdout[-2000:]} {result.stderr[-3000:]}") return result def start(self, phase): self.process = self.fixture.spawn([ self.binary, "--data-dir", self.data, "--admin-listen", f"127.0.0.1:{self.admin_port}", "--update-check-interval-secs", "0", "serve", "--proxy-listen", f"127.0.0.1:{self.reverse_port}", "--max-connections", "4096", "--max-inflight", "4096"], f"{self.label}-{phase}", env=self.env, affinity=self.fixture.server_cpus) wait_ready(self.admin_port, self.process) wait_ready(self.reverse_port, self.process) def apply(self, config, name, expected_success=True): config_path = self.fixture.root / f"{self.label}-{name}.json" write_json(config_path, config) result = self.cli("config", "apply", str(config_path), check=False) # Configurations with ephemeral test passwords/hashes are intentionally not copied. safe = copy.deepcopy(config) for vhost in safe.get("vhosts", []): if "basic_auth" in vhost: vhost["basic_auth"]["users"] = "REDACTED fixture users" write_json(self.fixture.results / "configs" / f"{self.label}-{name}.json", safe) event = {"name": name, "sha256": sha256(config_path), "returncode": result.returncode, "expected_success": expected_success, "stdout": result.stdout[-2000:], "stderr": result.stderr[-3000:]} self.applies.append(event) write_json(self.fixture.results / f"{self.label}-config-applies.json", self.applies) if (result.returncode == 0) != expected_success: raise RuntimeError(f"config {name}: {result.stdout} {result.stderr}") return result def reset(self): return self.apply(self.base, "restored-base") def login(self): status, headers, body = http(self.admin_port, "/api/auth/login", "POST", {"Origin": f"http://127.0.0.1:{self.admin_port}", "Content-Type": "application/json", "X-RustProxy-Request": "1"}, json.dumps({"username": self.username, "password": self.password})) if status != 200: raise RuntimeError(f"fixture login failed: HTTP {status}: {body[:200]!r}") self.cookie = headers["set-cookie"].split(";", 1)[0] self.csrf = json.loads(body)["csrf_token"] return status def wait_quiescent(self, timeout=180, quiet_seconds=5, poll_seconds=.5): """Wait for the real, one-time ACME bootstrap before any timed work. This does not disable ACME or change the process environment. The private fixture has no issuers/jobs; a terminal unavailable state is valid for HTTP tests, but does not establish ACME issuance support. """ from resources import descendants, process_stats if timeout <= 0 or quiet_seconds <= 0 or poll_seconds <= 0 or timeout <= quiet_seconds: raise ValueError("quiescence timeout must exceed the positive quiet period") started = time.monotonic() deadline = started + timeout output = self.fixture.results / f"{self.label}-quiescence.json" report = {"completed": False, "label": self.label, "pid": self.process.pid, "timeout_seconds": timeout, "required_quiet_seconds": quiet_seconds, "poll_seconds": poll_seconds, "observations": [], "acme_issuance_tested": False} def read_admin(path): remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"{self.label} did not become quiescent within {timeout}s") if self.process.poll() is not None: raise RuntimeError(f"{self.label} exited during quiescence") status, _, body = self.admin(path, timeout=min(1, remaining)) if status != 200: raise RuntimeError(f"{self.label} quiescence {path}: HTTP {status}") return json.loads(body) try: root = process_stats(self.process.pid) if root is None: raise RuntimeError(f"{self.label} has no live root process") identity = (root["pid"], root["start_ticks"]) report["start_ticks"] = root["start_ticks"] envelope = read_admin("/api/config") if not isinstance(envelope, dict) or not isinstance(envelope.get("config"), dict): raise RuntimeError("quiescence requires the actual configuration envelope") certificates = envelope["config"].get("tls", {}).get("certificates", []) if not isinstance(certificates, list) or any(not isinstance(cert, dict) for cert in certificates): raise RuntimeError("unexpected certificate configuration during quiescence") report["configured_issuers"] = sum(cert.get("issuer") is not None for cert in certificates) if report["configured_issuers"]: raise RuntimeError("performance fixture must have no configured ACME issuers") quiet_started, previous_terminal = None, None while True: payload = read_admin("/api/acme") if not isinstance(payload, dict) or not {"runtime", "job", "renewals"} <= payload.keys(): raise RuntimeError("quiescence requires explicit ACME runtime/job/renewal state") if payload["job"] is not None or payload["renewals"] != {}: raise RuntimeError("performance fixture has an unexpected ACME job or renewal") runtime = payload["runtime"] if not isinstance(runtime, dict) or runtime.get("status") not in { "checking", "initializing", "ready", "unavailable"}: raise RuntimeError("unexpected ACME runtime state during quiescence") pids = descendants(self.process.pid) root = process_stats(self.process.pid) if root is None or (root["pid"], root["start_ticks"]) != identity: raise RuntimeError(f"{self.label} root process identity changed during quiescence") now = time.monotonic() if now >= deadline: raise TimeoutError(f"{self.label} did not become quiescent within {timeout}s") terminal = tuple(runtime.get(key) for key in ("status", "mode", "message")) quiet = runtime["status"] in {"ready", "unavailable"} and pids == [self.process.pid] if not quiet: quiet_started, previous_terminal = None, None elif quiet_started is None or terminal != previous_terminal: quiet_started, previous_terminal = now, terminal elapsed_quiet = now - quiet_started if quiet_started is not None else 0 report["observations"].append({ "elapsed_seconds": now - started, "process_count": len(pids), "acme_status": runtime["status"], "acme_mode": runtime.get("mode"), "acme_message": runtime.get("message"), "quiet_seconds": elapsed_quiet}) if elapsed_quiet >= quiet_seconds: report.update({"completed": True, "elapsed_seconds": now - started, "terminal_status": runtime["status"], "process_count": len(pids)}) write_json(output, report) return report write_json(output, report) time.sleep(min(poll_seconds, max(0, deadline - time.monotonic()))) except (Exception, KeyboardInterrupt) as error: report.update({"elapsed_seconds": time.monotonic() - started, "error_type": type(error).__name__, "error": str(error)}) write_json(output, report) raise def admin(self, path, method="GET", body=None, csrf=True, cookie=True, origin=None, timeout=10): headers = {"Origin": origin or f"http://127.0.0.1:{self.admin_port}", "X-RustProxy-Request": "1"} if cookie: headers["Cookie"] = self.cookie if csrf: headers["X-CSRF-Token"] = self.csrf if body is not None: headers["Content-Type"] = "application/json" body = json.dumps(body) return http(self.admin_port, path, method, headers, body, timeout=timeout)