

Overview
Peregrine is a TLS 1.3 reverse proxy written in Rust that terminates hybrid post-quantum key exchange — X25519MLKEM768, IANA codepoint 0x11EC — in front of an ordinary HTTP origin. It uses real ML-KEM-768 from aws-lc-rs, not a stub. The point of the project is a measurement: for every connection it records which key-exchange group was negotiated, how long the handshake took and how many bytes it put on the wire, so the question "how much does post-quantum TLS cost me?" gets a real answer instead of a guess.
Clients that only speak classical X25519 fall back to it by default; one setting in the TOML config turns the proxy hybrid-only and rejects them.
Technologies
- - Rust
- - rustls 0.23 + aws-lc-rs
- - tokio + tokio-rustls
- - hyper 1
- - Prometheus metrics
- - TOML config, rcgen

Measured cost of the hybrid upgrade per TLS 1.3 handshake (loopback)

Year
2026
Type
Systems / Cryptography Project
Role
Solo Developer
Highlights
Terminated real X25519MLKEM768 TLS 1.3 handshakes in front of a plain HTTP origin, with classical X25519 as an opt-out fallback.
Measured the cost on loopback: a hybrid handshake came to about 3305 bytes and 59 ms, against about 994 bytes and 30 ms for classical X25519 — same certificate, same topology, one config knob.
That is roughly 3.3x the bytes and 2x the latency for the post-quantum upgrade, exported per connection as Prometheus metrics.

Hybrid (loopback)
≈3305 B / 59 ms
Classical (loopback)
≈994 B / 30 ms
Technical Implementation
TLS termination: rustls 0.23 with its post-quantum preference enabled and aws-lc-rs as the crypto provider, driven through tokio-rustls. Decrypted requests are forwarded to the origin with hyper 1 and hyper-util.
Per-connection telemetry: Each connection records the negotiated key-exchange group, handshake latency and handshake bytes in both directions, and feeds counters that split hybrid from classical connections.
Separate admin port: A Prometheus-style /metrics endpoint is served on its own admin listener, so scraping never shares a port with proxied traffic.
Config and certificates: A single peregrine.toml controls listeners, the origin and the hybrid_only switch. rcgen generates self-signed certificates for local runs.
Measuring from both ends: A demo client reports the same handshake numbers from its side, with a --classical-only flag to produce the comparison run. An integration test spins up a mock upstream, the proxy and a client, and asserts the negotiated group is 0x11EC.

Key exchange
X25519MLKEM768 (0x11EC)
Fallback
X25519, configurable
Design Decisions & Challenges
The comparison only means something if one variable changes. Both runs use the same certificate, the same topology and the same proxy; the only difference is whether the client offers the hybrid group. That is what makes the 3.3x byte and 2x latency figures a fair answer rather than a benchmark of two different setups.
Byte accounting was the subtle part. rustls pipelines the first application record into the last handshake read from the socket, so subtracting a snapshot taken at handshake completion under-counts. The counters report total wire bytes instead, and keeping that invariant is a rule for any future change.
Falling back to classical X25519 by default keeps the proxy usable with clients that do not yet support ML-KEM, while hybrid_only lets an operator enforce post-quantum key exchange once they are ready to turn older clients away.
