#!/usr/bin/env python3 """Real, bounded RustProxy H2/TLS, H3/QUIC and H1 WebSocket acceptance. Install Python dependencies aioquic, httpx[http2] and websockets. Example: python3 scripts/performance/transport_regression.py --binary before=/abs/binary \ --binary after=/abs/binary --out /abs/results --python-deps /abs/optional/pydeps Only fresh temporary databases and loopback listeners are used. Certificates are generated by RustProxy and verified by clients; no protocol fallback is used. This is an interoperability/functional suite, not a throughput benchmark. """ from __future__ import annotations import argparse import asyncio import copy import hashlib import importlib.metadata import json from pathlib import Path import platform import socket import sys import time import traceback from common import Fixture, seal_results, sha256, write_json from peer import Peer def response_details(response): return {"status": response.status, "protocol": response.protocol, "alpn": response.alpn, "stream_id": response.stream_id, "bytes": len(response.body), "sha256": hashlib.sha256(response.body).hexdigest(), "cache": response.headers.get("x-cache"), "cache_reason": response.headers.get("x-cache-reason")} def tls_port(fixture): while True: port = fixture.port() with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp: try: udp.bind(("127.0.0.1", port)) return port except OSError: continue def transport_config(rust, reverse_tls, static_tls, websocket_port, websocket_origin): config = copy.deepcopy(rust.base) for source, port in [("reverse", reverse_tls), ("static", static_tls)]: identifier = f"{source}-tls" config["listeners"]["endpoints"].append({ "id": identifier, "name": identifier, "scope": "internal", "enabled": True, "binding": {"address": f"127.0.0.1:{port}", "tls": True, "http2": True, "http3": {"max_concurrent_connections": 16, "max_concurrent_bidistream": 16, "max_concurrent_unistream": 16, "max_idle_timeout": 10}}}) vhost = next(v for v in config["vhosts"] if v["id"] == source) vhost["frontend"]["listener_ids"].append(identifier) vhost["http2_enabled"] = True vhost["http3_enabled"] = True config["listeners"]["endpoints"].append({ "id": "websocket", "name": "WebSocket", "scope": "internal", "enabled": True, "binding": {"address": f"127.0.0.1:{websocket_port}", "tls": False, "http2": False, "http3": None}}) config["vhosts"].append({ "id": "websocket", "frontend": {"hosts": ["127.0.0.1", "localhost"], "listener_ids": ["websocket"]}, "websockets": True, "backends": [{"id": "echo", "url": f"http://127.0.0.1:{websocket_origin}", "allow_private": True}], "load_balancing": {"health_check": {"mode": "off"}}, "routes": []}) config["cache"].update({"enabled": False, "expose_status": True, "expose_hits": True, "expose_cache_status": True, "expose_only_hits": False}) config["compression"] = {"gzip": False, "br": False, "zstd": False, "deflate": False} config["web_services"][0]["cache_control"] = "public, max-age=60" return config async def http_cases(check, session, protocol, source, cache, fixture): prefix = f"{protocol}.{source}.cache_{'on' if cache else 'off'}" expected = (fixture.site / "small.html").read_bytes() path = f"/transport-{protocol}.html" first = await session.request("GET", path) check(prefix + ".get", first.status == 200 and first.body == expected, **response_details(first)) second = await session.request("GET", path) check(prefix + ".cache_delivery", second.status == 200 and second.body == expected and ((first.headers.get("x-cache") == "MISS" and second.headers.get("x-cache") == "HIT") if cache else second.headers.get("x-cache") not in ("MISS", "HIT")), first_cache=first.headers.get("x-cache"), **response_details(second)) head = await session.request("HEAD", path) check(prefix + ".head", head.status == 200 and head.body == b"" and head.headers.get("content-length") == str(len(expected)), **response_details(head)) etag = second.headers.get("etag") check(prefix + ".validator", bool(etag), etag=etag) ranged = await session.request("GET", path, {"range": "bytes=11-40", "if-range": etag or ""}) check(prefix + ".range", ranged.status == 206 and ranged.body == expected[11:41] and ranged.headers.get("content-range") == f"bytes 11-40/{len(expected)}", **response_details(ranged)) mismatch = await session.request("GET", path, {"range": "bytes=11-40", "if-range": '"different"'}) check(prefix + ".if_range_mismatch", mismatch.status == 200 and mismatch.body == expected, **response_details(mismatch)) conditional = await session.request("GET", path, {"if-none-match": etag or ""}) check(prefix + ".conditional_304", conditional.status == 304 and conditional.body == b"", **response_details(conditional)) invalid = await session.request("GET", path, {"range": "bytes=999999-"}) check(prefix + ".range_416", invalid.status == 416 and invalid.body == b"" and invalid.headers.get("content-range") == f"bytes */{len(expected)}", **response_details(invalid)) for name in ["large.bin", "huge.bin"]: expected_file = (fixture.site / name).read_bytes() response = await session.request("GET", "/" + name) check(prefix + ".get." + name, response.status == 200 and response.body == expected_file, **response_details(response)) parallel = await asyncio.gather(*(session.request("GET", "/large.bin") for _ in range(4))) expected_large = (fixture.site / "large.bin").read_bytes() check(prefix + ".multiplex_four_streams", all(r.status == 200 and r.body == expected_large for r in parallel) and len({r.stream_id for r in parallel}) == 4, stream_ids=[r.stream_id for r in parallel], response_bytes=[len(r.body) for r in parallel]) if source == "reverse": for payload in [b"", (bytes(range(256)) * 512) + b"streamed-end"]: response = await session.request("POST", "/echo?transport=preserved", {"content-type": "application/octet-stream"}, payload) data = json.loads(response.body) if response.status == 200 else {} check(prefix + f".post_stream_{len(payload)}", response.status == 200 and data.get("method") == "POST" and data.get("body_bytes") == len(payload) and data.get("body_sha256") == hashlib.sha256(payload).hexdigest() and data.get("path") == "/echo?transport=preserved", upload_bytes=len(payload), **response_details(response)) else: response = await session.request("POST", path, body=b"static-method-gate") check(prefix + ".post_static_method_gate", response.status == 405, **response_details(response)) async def websocket_cases(check, port, cache): import websockets prefix = f"h1.websocket.cache_{'on' if cache else 'off'}" async with websockets.connect(f"ws://127.0.0.1:{port}/echo", open_timeout=5, close_timeout=3, ping_interval=None, compression=None) as session: text = "RustProxy echo: Zażółć gęślą jaźń" await session.send(text) returned = await asyncio.wait_for(session.recv(), timeout=5) check(prefix + ".text_echo", returned == text) binary = bytes(range(256)) * 256 await session.send(binary) returned = await asyncio.wait_for(session.recv(), timeout=5) check(prefix + ".binary_echo", returned == binary, bytes=len(returned)) pong = await session.ping(b"rustproxy-transport") await asyncio.wait_for(pong, timeout=5) check(prefix + ".ping_pong", True) await session.close(code=1000, reason="fixture complete") check(prefix + ".normal_close", session.close_code == 1000, close_code=session.close_code) async def run_variant(binary, label, output, workers): import websockets from transport_clients import client cases = [] started = time.monotonic() def check(case, passed, **details): cases.append({"case": case, "passed": bool(passed), **details}) print(f"{label} {'PASS' if passed else 'FAIL'} {case}", flush=True) write_json(output / "checks.json", cases) try: with Fixture(output, workers=workers) as fixture: plain = (fixture.site / "small.html").read_bytes() for name in ["transport-h2.html", "transport-h3.html", "transport-shared.html"]: (fixture.site / name).write_bytes(plain) peer = Peer(fixture.origin_port, fixture.site) websocket_origin, websocket_port = fixture.port(), fixture.port() async def echo(session, _path=None): async for message in session: await session.send(message) try: async with websockets.serve(echo, "127.0.0.1", websocket_origin, ping_interval=None, compression=None, close_timeout=3): rust = fixture.rust(binary, label) reverse_tls, static_tls = tls_port(fixture), tls_port(fixture) config = transport_config(rust, reverse_tls, static_tls, websocket_port, websocket_origin) rust.apply(config, "transport-listeners") fixture.stop(rust.process) rust.start("transport") certificate = rust.data / "certs" / "default.pem" write_json(output / "transport.json", { "certificate_sha256": sha256(certificate), "certificate_verification": True, "h2_alpn_required": "h2", "h3_alpn_required": "h3", "reverse_tls_tcp_udp": reverse_tls, "static_tls_tcp_udp": static_tls, "websocket_h1_tcp": websocket_port, "fallback_allowed": False}) for cache in [False, True]: if cache: config["cache"]["enabled"] = True rust.apply(config, "transport-cache-on") for protocol in ["h2", "h3"]: for source, port in [("reverse", reverse_tls), ("static", static_tls)]: group = f"{protocol}.{source}.cache_{'on' if cache else 'off'}" try: async with client(protocol, port, certificate) as session: await http_cases(check, session, protocol, source, cache, fixture) except Exception as exc: check(group + ".runner", False, error=str(exc), traceback=traceback.format_exc()) try: await websocket_cases(check, websocket_port, cache) except Exception as exc: check(f"h1.websocket.cache_{'on' if cache else 'off'}.runner", False, error=str(exc), traceback=traceback.format_exc()) for source, port in [("reverse", reverse_tls), ("static", static_tls)]: try: async with client("h2", port, certificate) as h2: warm = await h2.request("GET", "/transport-shared.html") async with client("h3", port, certificate) as h3: hit = await h3.request("GET", "/transport-shared.html") check(f"cache_cross_protocol.{source}.h2_to_h3", warm.status == 200 and hit.status == 200 and hit.body == plain and hit.headers.get("x-cache") == "HIT", first_cache=warm.headers.get("x-cache"), **response_details(hit)) except Exception as exc: check(f"cache_cross_protocol.{source}.runner", False, error=str(exc), traceback=traceback.format_exc()) check("runtime.still_alive", rust.process.poll() is None) finally: peer.close() except Exception as exc: check("runtime.runner", False, error=str(exc), traceback=traceback.format_exc()) report = {"label": label, "simulation": False, "seconds": time.monotonic() - started, "passed": sum(case["passed"] for case in cases), "failed": sum(not case["passed"] for case in cases), "cases": cases} write_json(output / "report.json", report) print(f"{label}: {report['passed']} passed, {report['failed']} failed", flush=True) return report def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--binary", action="append", required=True, metavar="LABEL=/ABS/PATH") parser.add_argument("--out", type=Path, required=True) parser.add_argument("--workers", type=int, default=4) parser.add_argument("--python-deps", type=Path) args = parser.parse_args() if args.python_deps: sys.path.insert(0, str(args.python_deps.resolve())) variants = {} for value in args.binary: label, separator, path = value.partition("=") if not separator or not label.replace("-", "").replace("_", "").isalnum() or label in variants: parser.error("--binary must be unique SIMPLE_LABEL=/ABS/PATH") variants[label] = Path(path).resolve() args.out = args.out.resolve() args.out.mkdir(parents=True, exist_ok=True) from transport_clients import validate_head_adapter write_json(args.out / "environment.json", { "kind": "real RustProxy transport acceptance", "simulation": False, "python": sys.version, "platform": platform.platform(), "dependencies": {name: importlib.metadata.version(name) for name in ["aioquic", "httpx", "h2", "websockets"]}, "aioquic_head_adapter": validate_head_adapter(), "started_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "not_covered": ["production certificates", "TLS upstreams", "H3 upstreams", "H2/H3 WebSocket extended CONNECT", "network loss or migration", "24-hour soak", "real-browser navigation"]}) reports = {} for label, binary in variants.items(): reports[label] = asyncio.run(run_variant(binary, label, args.out / label, args.workers)) write_json(args.out / "comparison.json", { "variants": {label: {key: report[key] for key in ["passed", "failed", "seconds"]} for label, report in reports.items()}, "cases": [{"case": case, **{label: next((row["passed"] for row in report["cases"] if row["case"] == case), "not_run") for label, report in reports.items()}} for case in sorted({row["case"] for report in reports.values() for row in report["cases"]})]}) seal_results(args.out) return 1 if any(report["failed"] for report in reports.values()) else 0 if __name__ == "__main__": sys.exit(main())