# pqcrypta-proxy benchmark configuration. # # Matched to the HAProxy config: same certificate, same backend, same worker count, # TLS 1.3 only. Every security feature that HAProxy has no equivalent for is # OFF for the like-for-like run — WAF, fingerprinting, rate limiting, audit # logging. A second run turns them back on, because that is the configuration # anyone actually deploys and the cost of it is a fair thing to publish. version = 1 [server] bind_address = "10.99.0.1" udp_port = 18444 # One physical core each (CPUs 4-5 are SMT siblings), two workers each, matched # to HAProxy nbthread 2. The backend needs two physical cores of its own: on one # it saturated and became the ceiling in ten of HAProxy's eighteen cells. worker_threads = 2 max_connections = 60000 enable_ipv6 = false graceful_shutdown_timeout_secs = 1 # The extension is negotiated, so leaving it on cannot disadvantage HAProxy — # but multipath is off so both sides run a plain single-path QUIC connection. max_concurrent_multipath_paths = 1 enable_ack_frequency = true enable_quic_retry = false enable_quic_migration = true # This box already runs a production pqcrypta-proxy node. The bench instance must # not touch its ports: WebTransport off its default 4433, and the port-80 HTTP # redirect server disabled outright (see [http_redirect]). webtransport_port = 18445 [tls] cert_path = "/root/bench/certs/bench-sec.local.crt" key_path = "/root/bench/certs/bench-sec.local.key" min_version = "1.3" alpn_protocols = ["h3", "h2", "http/1.1"] ocsp_stapling = false enable_0rtt = false pqc_session_tickets = false [pqc] # Classical for the like-for-like run. The PQC handshake cost is measured # separately, against HAProxy also configured for X25519MLKEM768, so that # number is a comparison rather than a penalty applied to one side only. enabled = false [backends.static] name = "static" type = "http1" address = "127.0.0.1:18080" tls = false timeout_ms = 30000 max_connections = 20000 [[routes]] name = "all" host = "bench-sec.local" path_prefix = "/" backend = "static" # Both arms must handle protocols identically or the comparison includes the # route gate. The gate is enforced only on the fingerprinting accept loop, so # without this the security-on arm answers 426 to every HTTP/1.1 request while # the security-off arm serves them. allow_http11 = true [waf] enabled = true mode = "block" scan_json_body = true [fingerprint] enabled = true tls_layer_capture = true # Classification, replay detection and drift detection all still run — that work # is exactly what this arm is pricing. What is lifted is the ability to ban: # h2load's JA3 classifies as Suspicious, and the default threshold of 100 # requests per minute bans a load generator inside the warm-up. block_scanners = false block_malicious = false suspicious_rate_threshold = 4000000000 suspicious_rate_window_secs = 1 suspicious_block_duration_secs = 1 malicious_block_duration_secs = 1 replay_detection = true drift_detection = true [rate_limiting] enabled = true # Far above the offered load: the cost under test is evaluating a request, and a # run that trips the limiter measures the rejection path instead. requests_per_second = 4000000 burst_size = 2000000 connection_rate_limit = true connections_per_second = 1000000 [advanced_rate_limiting] enabled = true [advanced_rate_limiting.global_limits] requests_per_second = 4000000 burst_size = 2000000 [advanced_rate_limiting.global_limits.per_ip] requests_per_second = 4000000 burst_size = 2000000 requests_per_minute = 240000000 requests_per_hour = 4000000000 [advanced_rate_limiting.global_limits.per_fingerprint] requests_per_second = 4000000 burst_size = 2000000 requests_per_minute = 240000000 requests_per_hour = 4000000000 [security] dos_protection = true # The generator is one IP holding a hundred connections; the default cap of 100 # sits exactly on that edge. max_connections_per_ip = 60000 [logging] level = "error" access_log = false audit_log_enabled = false [admin] enabled = false [ocsp] enabled = false [acme] enabled = false [cache] enabled = false [otel] enabled = false [conformance] enabled = false [masque] enabled = false [http_redirect] # Off: the default binds port 80, which the production node on this box owns. enabled = false [connection_pool] # Matched to what production actually runs, and comparable to HAProxy's # maxconn 20000 on the same backend. Leaving this at the built-in default of 10 # idle connections per host meant that at concurrency 100 most requests opened a # fresh backend TCP connection while HAProxy reused freely — which is a # difference between two configs, not between two proxies. idle_timeout_secs = 90 max_idle_per_host = 500 max_connections_per_host = 20000 acquire_timeout_ms = 5000 [http3] # OFF for the like-for-like run. These default to on, and each is per-request # work HAProxy is not doing: coalescing takes a DashMap lookup and possible # subscriber registration on every GET, early hints builds and writes a 103 # before the real response, priority hints parses and emits a Priority header. # They are real features and the default is defensible — but a run that leaves # them on while HAProxy does plain proxying is not the like-for-like comparison # it claims to be. conf/pqc-bench-features.toml keeps them on for the second # arm, which is the number anyone actually deploying this should read. early_hints_enabled = false priority_hints_enabled = false coalescing_enabled = false preconnect_origins = [] [headers] # Same reasoning: Server-Timing instruments every request and emits a header. server_timing_enabled = false