I maintain Qeli, a self-hosted VPN whose core and server are written in Rust. For the 0.7.x line I added a hybrid post-quantum key exchange to the inner handshake, and wired the same primitive into the non-Rust clients. Here is how it is built and what bit me.
Why hybrid, not pure PQ
The threat is "harvest now, decrypt later": traffic captured today, decrypted once a large quantum computer exists. Classical X25519 does not survive that; pure ML-KEM is young, and I do not want one new primitive to be the only thing between you and plaintext. So the handshake runs both and mixes the results - you are safe unless both X25519 and ML-KEM-768 fall.
The shape of the handshake
Classical: X25519 ephemeral ECDH.
Post-quantum: ML-KEM-768 (FIPS 203), via the RustCrypto stack - one side encapsulates to the other's ephemeral KEM public key.- The two shared secrets are concatenated and run through HKDF-SHA256 to derive the session keys. The data plane is ChaCha20-Poly1305; password-derived secrets use Argon2id.
Mixing both secrets in the KDF (instead of picking one) is what makes it hybrid: an attacker has to break both to recover the key.
One PQ core across Rust, C# and Kotlin
Qeli has native clients on Windows/macOS (C#) and Android (Kotlin). Rather than reimplement ML-KEM three times, the Rust implementation is the single source of truth, and the other clients call into a small native core over FFI (C#) and JNI (Android). Same wire format everywhere, and the PQ code gets reviewed once.
Things that bit me
Wire compatibility. Turning on PQ changed the handshake bytes. I kept it wire-compatible within 0.7.x, but it is a breaking change versus older clients - versioning the handshake from day one would have saved pain.
Message sizes. ML-KEM-768 public keys and ciphertexts are ~1.1 KB, far larger than X25519's 32 bytes. Worth accounting for in your framing and MTU assumptions.
Keep the classical path. It is tempting to drop X25519 once PQ works, but hybrid is the whole point - the KDF step has to bind both.
Try it
Code is open (AGPL-3.0 core, MPL-2.0 clients): https://github.com/litvinovtd/qeli - and there is a project site at https://qeli.ru. Feedback on the handshake and transport code is very welcome.