#!/usr/bin/env python3 """Verify real Nginx TLS HTTP/2 upstreams inside one bounded, isolated container.""" import argparse from datetime import datetime, timezone import hashlib import json from pathlib import Path import re import shutil import signal import subprocess import tempfile import time import uuid class ContainerRunner: def __init__(self, docker, output, deadline): self.docker = docker self.output = output self.deadline = deadline self.commands = [] self.cleaned = [] def execute(self, command): remaining = self.deadline - time.monotonic() if remaining <= 0: raise TimeoutError("HTTP/2 proxy verification exceeded its wall timeout") self.commands.append(command) result = subprocess.run(command, stdout=self.output, stderr=subprocess.STDOUT, timeout=remaining) self.output.flush() if result.returncode: raise RuntimeError(f"command failed with exit code {result.returncode}: {command[1:3]}") def container(self, name, arguments): command = [ self.docker, "run", "--rm", "--name", name, "--cpus", "2", "--memory", "2048m", "--memory-swap", "2048m", "--pids-limit", "256", "--log-opt", "max-size=10m", "--log-opt", "max-file=1", ] + arguments try: self.execute(command) finally: # Always finish cleanup before starting the next stage. Never prune # or target a container belonging to another verification run. cleanup = subprocess.run( [self.docker, "rm", "--force", name], capture_output=True, text=True, timeout=15, ) if cleanup.returncode and "No such container" not in cleanup.stderr: raise RuntimeError(f"could not clean up {name}: {cleanup.stderr.strip()}") self.cleaned.append(name) def sourceManifest(root): paths = [root / "go.mod", root / "go.sum", Path(__file__).resolve()] paths += list((root / "transport" / "http2").glob("*.go")) return {str(path.relative_to(root)): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(paths)} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--image", default="golang:1.25", help="compiler image") parser.add_argument("--nginx-image", help="Nginx image supporting proxy_http_version 2; defaults to nginx:1.30.5-alpine, or nginx:1.30.5 for --race") parser.add_argument("--race", action="store_true", help="instrument the integration test with Go's race detector; requires a glibc-based runtime image") parser.add_argument("--goproxy", default="") parser.add_argument("--count", type=int, default=3, help="repeat the proxy scenarios (default: 3)") parser.add_argument("--wall-timeout", type=int, default=600, help="total pull/build/test deadline in seconds") parser.add_argument("--output", type=Path, default=Path("performance/http2ProxyLinux.json")) args = parser.parse_args() if args.nginx_image is None: args.nginx_image = "nginx:1.30.5" if args.race else "nginx:1.30.5-alpine" if args.count < 1 or args.count > 10 or args.wall_timeout < 1: parser.error("--count must be 1..10 and --wall-timeout must be positive") docker = shutil.which("docker") if not docker: raise SystemExit("docker is required") root = Path(__file__).resolve().parent.parent args.output = args.output.resolve() args.output.parent.mkdir(parents=True, exist_ok=True) logPath = args.output.with_suffix(".txt") runID = uuid.uuid4().hex[:12] names = [f"go-omni-http2-proxy-{runID}-{stage}" for stage in ("build", "check")] capture = lambda command: subprocess.check_output(command, text=True, timeout=15).strip() running = capture([docker, "ps", "--format", "{{.ID}} {{.Names}}"]) if any("go-omni-http2-" in row for row in running.splitlines()): raise SystemExit("another HTTP/2 validation container is running; wait for it to finish") before = {row.split()[0] for row in running.splitlines()} report = { "status": "running", "startedAt": datetime.now(timezone.utc).isoformat(), "testedCommit": capture(["git", "-C", str(root), "rev-parse", "HEAD"]), "workingTreeStatus": capture(["git", "-C", str(root), "status", "--porcelain"]), "sourceSha256": sourceManifest(root), "repetitions": args.count, "race": args.race, "containerLimits": {"cpus": 2, "memoryBytes": 2147483648, "memoryAndSwapBytes": 2147483648, "pidsLimit": 256}, "wallTimeoutSeconds": args.wall_timeout, "imageIds": {}, "notes": [ "Compiler and integration stages run sequentially; at most one 2-CPU/2-GiB validation container is active.", "The integration container has network=none, no published ports and a read-only root filesystem. Client, Nginx and origins communicate over loopback inside that container.", "Both TLS legs validate certificates; invalid upstream CA and hostname must fail before reaching the handler.", "HTTP/2 is verified independently on both legs. This is a functional integration test, not a throughput benchmark or a live Cloudflare deployment.", ], } args.output.write_text(json.dumps(report, indent=2) + "\n") def interrupt(signum, frame): raise KeyboardInterrupt handlers = {sig: signal.signal(sig, interrupt) for sig in (signal.SIGTERM, signal.SIGHUP)} failure = None with logPath.open("w") as output: runner = ContainerRunner(docker, output, time.monotonic() + args.wall_timeout) try: for imageName in (args.image, args.nginx_image): inspected = subprocess.run([docker, "image", "inspect", imageName, "--format", "{{.Id}}"], capture_output=True, text=True, timeout=15) if inspected.returncode: print(f"Pulling {imageName}", flush=True) runner.execute([docker, "pull", imageName]) report["imageIds"][imageName] = capture([docker, "image", "inspect", imageName, "--format", "{{.Id}}"]) with tempfile.TemporaryDirectory(prefix="goOmniHTTP2Proxy") as temporary: print("Building proxy integration tests; limits: 2 CPUs, 2048 MiB, 256 tasks", flush=True) arguments = [ "-v", f"{root}:/workspace:ro", "-v", f"{temporary}:/output", "-w", "/workspace", "-e", "GOWORK=off", "-e", "GOMAXPROCS=2", "-e", f"CGO_ENABLED={1 if args.race else 0}", ] if args.goproxy: arguments += ["-e", f"GOPROXY={args.goproxy}"] raceFlag = "-race " if args.race else "" arguments += [args.image, "sh", "-c", f"go version && go vet ./transport/http2 && go test {raceFlag}-c -o /output/http2.test ./transport/http2"] runner.container(names[0], arguments) print(f"Running Nginx HTTP/2 proxy scenarios, {args.count} repetitions; external networking disabled", flush=True) runner.container(names[1], [ "--network", "none", "--read-only", "--tmpfs", "/tmp:rw,nosuid,noexec,size=128m", "-v", f"{temporary}:/test:ro", "--entrypoint", "/test/http2.test", "-e", "GO_OMNI_HTTP2_PROXY_TEST=1", "-e", "GOMAXPROCS=2", args.nginx_image, "-test.v", "-test.run", "^TestNginxHTTP2Proxy$", "-test.timeout", "120s", "-test.count", str(args.count), ]) log = logPath.read_text() for case in ("TLSAndConnectionReuse", "TimeoutAndCancellation", "GoAwayAndRollingDrain"): if len(re.findall(r"--- PASS: TestNginxHTTP2Proxy/" + case + r" \(", log)) != args.count: raise RuntimeError("missing successful integration case: " + case) report["proxyVersions"] = sorted(set(re.findall(r"proxyVersion=(.+)", log))) report["results"] = re.findall(r"proxyResult=(.+)", log) version = re.search(r"^go version (\S+) (\S+)$", log, re.MULTILINE) if version is None or not report["proxyVersions"]: raise RuntimeError("missing Go or Nginx version evidence") report["goVersion"], report["platform"] = version.groups() report["status"] = "passed" except (Exception, KeyboardInterrupt) as error: failure = str(error) or type(error).__name__ report["status"], report["error"] = "failed", failure finally: for sig, handler in handlers.items(): signal.signal(sig, handler) report["commands"] = runner.commands report["cleanedContainers"] = runner.cleaned report["finishedAt"] = datetime.now(timezone.utc).isoformat() output.flush() report["logSha256"] = hashlib.sha256(logPath.read_bytes()).hexdigest() try: after = set(capture([docker, "ps", "--format", "{{.ID}}"]).split()) if not before.issubset(after): raise RuntimeError("a pre-existing container is no longer running") report["preExistingContainersStillRunning"] = len(before) if set(runner.cleaned) != set(names) and failure is None: raise RuntimeError("container cleanup was not confirmed") except Exception as error: failure = str(error) report["status"], report["error"] = "failed", failure args.output.write_text(json.dumps(report, indent=2) + "\n") if failure: raise SystemExit(f"FAIL HTTP/2 proxy verification: {failure}; see {logPath}") print(f"PASS HTTP/2 Nginx proxy verification; report: {args.output}", flush=True) if __name__ == "__main__": main()