DEV Community

executor
executor

Posted on

I Built a Post-Quantum Tunnel With a Chrome 124 TLS Fingerprint

By Alessandro Faraone


Most "secure tunnel" tools share the same fundamental problem: they are detectable. VPN handshakes have fingerprints. Tor cells have patterns. Even obfuscated proxies leave statistical traces that commercial DPI systems can classify. An adversary who can see your packets does not need to decrypt them — they only need to identify them.

I spent months thinking about a different question: what if a tunnel produced traffic that was not just encrypted, but fingerprint-identical to a browser at the TLS layer? Not approximately similar — the same ClientHello structure, the same cipher suites, the same extension order, the same JA3/JA4 hashes that DPI classifiers extract to identify Chrome.

That question became Labyrinth-Mesh — a post-quantum resilient multi-path tunnel written in Rust.


The Threat Model

Before the architecture: what are we actually defending against?

The system is designed to resist an adversary who:

  • Intercepts and records traffic on a subset of paths
  • Runs ML-based Deep Packet Inspection (Snort, Zeek, or commercial classifiers)
  • Has access to a cryptographically-relevant quantum computer capable of breaking RSA and ECDH
  • Has physically compromised one or more relay nodes before the handshake
  • Injects replayed packets captured from previous sessions
  • Attempts multi-path flow correlation to identify the sender

This is a real threat model for journalism, human rights work, and high-value corporate communication. It is not theoretical.


Multi-Path as a Cryptographic Primitive

Most tunnels treat multi-path as a performance feature. In Labyrinth-Mesh it is a cryptographic property.

Every payload is split into 5 shares using Shamir Secret Sharing over GF(2⁸). Any 3 of the 5 shares are sufficient to reconstruct the original. Each share is authenticated with a BLAKE3 tag keyed on (session_key, seq, fragment_index).

The shares are sent over 5 independent network paths simultaneously. No single path ever carries enough information to reconstruct the payload. An adversary who intercepts 2 of the 5 paths gets nothing — not degraded security, literally zero information about the content.

Payload
  │
  ▼
GF(2⁸) Shamir SSS  →  5 shares, k=3 threshold
  │
  ▼
BLAKE3 auth tag     →  8 bytes per share
  │
  ▼
5 independent network paths
Enter fullscreen mode Exit fullscreen mode

This design means the tunnel is resilient not just to DPI but to partial path failure, selective blocking, and relay compromise — all simultaneously.


Post-Quantum Key Agreement

The session key uses a hybrid KEM combining two independent primitives:

Primitive Algorithm Security
Post-quantum Kyber-1024 (ML-KEM) Resistant to CRQC
Classical X25519 ECDH Resistant to classical MITM
session_key = BLAKE3-derive(
    "labyrinth-hybrid-v3 session",
    kyber_shared_secret  x25519_shared_secret
)
Enter fullscreen mode Exit fullscreen mode

Breaking one of the two layers does not compromise the session. A quantum computer can break X25519 — it cannot break Kyber-1024. A classical attacker cannot break Kyber or X25519. The session key is secure against both attack classes simultaneously.

Key ratchet: every 10,000 packets, the key is rotated via BLAKE3-KDF. Past keys cannot be derived from current keys. Forward secrecy is maintained at the packet level.

Receiver identity: the receiver's KEM public key is signed with Dilithium3 (ML-DSA), a post-quantum signature scheme. The sender can pin the receiver's fingerprint. This eliminates KEM MITM attacks even against quantum adversaries.


The DPI Problem and Protocol Steganography

Standard tunnel implementations fail against modern DPI because they produce distinctive packet sizes, timings, and byte patterns. Even when encrypted, the shape of traffic is revealing.

Labyrinth-Mesh addresses this at two layers.

Wire-Format Framing (UDP mode)

Each share is wrapped in a frame that mimics a legitimate protocol before being sent over UDP:

Path index % 4 Wire format Magic bytes
0 TLS 1.3 Application Data 17 03 03 <len>
1 QUIC Short Header 41 <DCID[8]> <pkt_num[2]>
2 WebSocket Binary (masked) 82 <len> <mask[4]> <masked>
3 HTTP/2 DATA frame <len[3]> 00 00 <stream_id[4]>

The inner payload is padded with ChaCha20 pseudo-random bytes derived from the session key to reach protocol-typical sizes (TLS ~480 B, QUIC ~500 B, HTTP/2 ~1500 B). The padding is computationally indistinguishable from real encrypted data.

A Markov-chain inter-arrival time model shapes packet timing to match observed protocol distributions.

HTTP/2 Mode — The Real Thing

Wire-format mimicry is good. Actual HTTP/2 with a Chrome 124 TLS fingerprint is better.

The --http flag switches the transport to real TLS 1.3 + HTTP/2. The receiver generates a self-signed certificate at runtime (via rcgen), exchanges its DER fingerprint over the Dilithium3-signed control channel, and starts a standard HTTPS server backed by tokio-rustls + hyper.

The sender uses a custom TLS 1.3 client — not a library — built from scratch in Rust. It constructs a ClientHello wire-identical to Chrome 124: all 17 extensions in Chrome's exact order, the same cipher suite list (with GREASE), and an X25519Kyber768Draft00 (0x6399) hybrid post-quantum key share. The JA3 and JA4 fingerprints of every connection are identical to Chrome 124 — the same hashes that passive network monitors and DPI classifiers compute to identify browser traffic.

The key schedule, AEAD (AES-128-GCM), and record layer follow RFC 8446 exactly. Certificate pinning is enforced by comparing the server's certificate DER byte-for-byte against the fingerprint received over the Dilithium3-authenticated control channel.

Each share is posted to https://receiver:port/s with:

  • User-Agent: Mozilla/5.0 ... Chrome/124.0.0.0
  • Bucket-padded body: [512, 1024, 2048, 4096, 8192] bytes
  • Inter-arrival timing sampled from an empirical Chrome distribution (P10 = 2 ms, P50 = 15–80 ms, P90 = 400 ms)

The result: a passive network observer performing TLS fingerprinting sees a standard Chrome 124 connection. DPI classifiers that extract JA3/JA4 hashes, extension order, or cipher lists cannot distinguish this traffic from a browser visiting a web server. Packet sizes match browser buckets. Timing matches real user behavior.

The TLS certificate provides transport encryption. The Dilithium3 signature on the ctrl channel provides receiver authentication. These are two orthogonal security layers — the self-signed cert does not need a CA because identity is established post-quantumly before TLS begins.


Threshold KEM — Eliminating the Single Point of Compromise

Standard KEM setups have a weakness: if the receiver is compromised before the handshake, the session key is exposed. Labyrinth-Mesh solves this with a Threshold KEM.

The receiver generates N independent Hybrid KEM sub-keypairs (--tkem-relays N). The sender:

  1. Generates a 32-byte master secret M
  2. Splits M into N Shamir shares with threshold K
  3. For each relay i: encapsulates with relay_pk_i, gets kem_secret_i, encrypts the share with BLAKE3("tkem-share-enc" ‖ kem_secret_i)
  4. Sends the full ciphertext bundle over the authenticated control channel

The receiver decapsulates all N ciphertext, recovers the N shares, reconstructs M, and both sides derive session_key = BLAKE3-derive("labyrinth-tkem-v1 session", M).

The security property: an adversary who compromises K−1 sub-keys obtains K−1 Shamir shares. With K−1 < K shares, the master secret M is information-theoretically secret on GF(256). No amount of computation recovers M from fewer than K shares. Even if a quantum computer breaks one of the sub-KEMs, the session key remains secure as long as fewer than K relay sub-keys are compromised.

labyrinth recv --tkem-relays 3
labyrinth send --to 127.0.0.1:8199 --tkem-threshold 2
Enter fullscreen mode Exit fullscreen mode

Adaptive Multi-Path Scheduler

Traffic is not distributed in fixed round-robin. Each path gets a continuous quality score:

score_new = α × rtt_component + (1−α) × (1 − loss_rate)
score     = 0.7 × score_old + 0.3 × score_new              [EWMA, α=0.3]
Enter fullscreen mode Exit fullscreen mode

Shamir shares are assigned to paths by weighted sampling proportional to score. Dead paths (loss > 85%) enter exponential backoff (1 s → 60 s) and are excluded until they recover.

Probe packets (20 bytes, sent every 500 ms) measure per-path latency. ICMP hard errors (ECONNREFUSED, ENETUNREACH, EHOSTUNREACH) immediately increment the loss counter. The receiver silently discards probes because their BLAKE3 auth tag is invalid.

This makes the tunnel self-healing: when a path degrades or is selectively blocked, traffic automatically migrates to healthy paths without session interruption.


Cover Traffic (XDP)

Even when traffic flows constantly, silence is revealing. If a sender goes idle, the absence of packets allows an adversary to infer activity patterns.

A BPF/XDP program runs in kernel space and maintains a constant bitrate (CBR) by injecting cover packets when the channel is idle. This eliminates traffic-silence correlation attacks.

The cover traffic program uses Linux eBPF (kernel ≥ 5.15) and runs with the scheduler at XDP_TX. It operates entirely in kernel space — the cover packets never reach userland.


Technical Stack

The full implementation is in Rust 2021:

pqcrypto-kyber     0.7    Kyber-1024 session KEM + Kyber-768 TLS key share
x25519-dalek       2      X25519 ECDH (session KEM + TLS key share)
pqcrypto-dilithium 0.5    Dilithium3 receiver identity signatures
blake3             1.5    Hash, KDF, auth tags, key derivation
sharks             0.5    Shamir SSS over GF(256)
chacha20           0.9    Padding CSPRNG
aes-gcm            0.10   AES-128-GCM AEAD for custom TLS 1.3 records
hmac               0.12   HMAC-SHA256 for TLS 1.3 Finished messages
sha2               0.10   SHA-256 transcript hash
rcgen              0.13   Runtime self-signed cert generation (receiver)
tokio-rustls       0.26   Async TLS 1.3 server (receiver side only)
hyper              1      HTTP/2 client + server
http-body-util     0.1    HTTP body helpers
axum               0.7    Management plane REST API + share endpoint
reqwest            0.12   HTTP client for management plane (TUI, status)
aya                0.12   eBPF userspace loader
tokio              1      Async runtime
Enter fullscreen mode Exit fullscreen mode

One non-obvious detail on the sender side: the custom TLS 1.3 client does not use any TLS library. It opens a raw TcpStream, writes the ClientHello bytes directly, parses the ServerHello, performs X25519 (or X25519+Kyber768) key exchange, derives handshake keys via HKDF, decrypts the server's Finished with AES-128-GCM, and sends the client Finished. The Chrome 124 extension order is hardcoded exactly — any deviation changes the JA3/JA4 hash.

On the receiver side, rustls 0.23 requires an explicit crypto provider before ServerConfig::builder():

rustls::crypto::ring::default_provider().install_default().ok();
Enter fullscreen mode Exit fullscreen mode

The .ok() is intentional — it silences the duplicate-install error on re-entry (internally a OnceLock).

hyper_util::server::conn::auto::Builder is used instead of the strict http2::Builder so the server handles both HTTP/1.1 and HTTP/2 transparently, in case TLS ALPN negotiation falls back.


What Makes This Different

Existing tools in this space generally pick one property:

  • Secure: use standard crypto, accept traffic fingerprinting
  • Stealthy: obfuscate, sacrifice some security guarantees
  • Multi-path: split traffic, but use classical crypto

Labyrinth-Mesh tries to provide all three simultaneously:

  1. Post-quantum secure: hybrid KEM against CRQC; Dilithium3 receiver identity
  2. Wire-undetectable in HTTP mode: actual TLS 1.3 + HTTP/2 with Chrome fingerprint, indistinguishable from browser traffic at the DPI layer
  3. Information-theoretically multi-path: Shamir SSS means no partial path subset reveals anything

The Threshold KEM adds a fourth property that most tunnels ignore entirely: resilience to relay compromise during the key setup phase.


Current Limitations

XDP cover traffic requires Linux ≥ 5.15 and root. Most deployment environments qualify; edge deployments on older kernels cannot use this feature.

Threshold KEM in the current implementation places all sub-keypairs on the same receiver machine. Full protection against physical relay compromise requires distributing sub-keypairs to independent hosts — the protocol supports this but the routing infrastructure is not yet built.

HTTP mode steganography defeats DPI-based classifiers and statistical analysis. It does not help if the receiver IP is layer-3 blocked. A future version will add domain-fronting support.

Markov IAT in UDP mode models parametric distributions, not empirically-captured real traffic. A well-resourced adversary with ML classifiers trained on real traffic could potentially distinguish it from genuine TLS/QUIC. HTTP mode does not have this limitation because it uses the actual protocol stack.


Running It

# UDP mode — 3 terminals
labyrinth recv --ctrl 0.0.0.0:8199 --udp 0.0.0.0:8200
labyrinth-tui --mgmt 127.0.0.1:9090
labyrinth send --to 127.0.0.1:8199

# HTTP/2 mode — TLS fingerprint identical to Chrome 124 (JA3/JA4)
labyrinth recv --ctrl 0.0.0.0:8199 --udp 0.0.0.0:443 --http --sign
labyrinth send --to 127.0.0.1:8199 --remotes 127.0.0.1:443 --http

# Threshold KEM — compromise-resilient key setup
labyrinth recv --tkem-relays 3
labyrinth send --to 127.0.0.1:8199 --tkem-threshold 2

# Container
docker-compose up
Enter fullscreen mode Exit fullscreen mode

Source: github.com/xilioscient/rinnegato


Alessandro Faraone — Feedback and security reports welcome at the repository.

Top comments (0)