v6.2 β Full Chrome TLS Impersonation (BoringSSL)
Critical Fix: Complete Browser Fingerprint Impersonation
The v6.1 fixes (Chrome headers, HTTP/2, cookies) were necessary but not sufficient. Cloudflare uses three fingerprinting layers simultaneously, and v6.1 only addressed one:
| Layer | What CF Checks | v6.1 Status | v6.2 Status |
|---|---|---|---|
| HTTP Headers | UA, Sec-CH-UA, Sec-Fetch-* | β Fixed | β (now via rquest) |
| TLS Fingerprint (JA3/JA4) | Cipher suites, extensions, GREASE | β FAILED (rustls) | β Fixed (BoringSSL) |
| HTTP/2 Fingerprint (Akamai) | SETTINGS frame values, window size | β FAILED (hyper defaults) | β Fixed (Chrome H2) |
Root Cause
reqwest + rustls generates a TLS ClientHello that is nothing like Chrome's:
rustlssends ~6 cipher suites in wrong order. Chrome sends 16 with GREASE.rustlsdoesn't supportcompress_certificate(brotli). Chrome does.rustlsdoesn't inject GREASE values. Chrome uses RFC 8701 GREASE.hyper's HTTP/2 sendsINITIAL_WINDOW_SIZE=65535. Chrome sends6291456(6MB).hypersends SETTINGS that produce a different Akamai H2 fingerprint.
Cloudflare detects this mismatch at the TLS handshake level β before any HTTP headers are even sent. No amount of header faking can fix a wrong JA3.
Solution: rquest with BoringSSL
Replaced reqwest (rustls) with rquest (BoringSSL-backed Chrome impersonation):
# OLD (v6.1):
reqwest = { version = "0.12", features = ["rustls-tls", "gzip", "brotli", "cookies", "http2"] }
# NEW (v6.2):
rquest = { version = "1.0", features = ["full"] }
rquest uses a patched BoringSSL (Chrome's actual TLS library) that emits a byte-for-byte identical TLS ClientHello to Chrome 137. It also configures HTTP/2 SETTINGS to match Chrome's exact values.
One-line initialization:
let client = Client::builder()
.impersonate(Impersonate::Chrome137)
.cookie_store(true)
.build()?;
This single call configures:
- β TLS cipher suite order (16 ciphers + GREASE, exact Chrome order)
- β TLS extension ordering (with compress_certificate, ALPS, GREASE)
- β HTTP/2 SETTINGS frame (INITIAL_WINDOW_SIZE=6291456, etc.)
- β Header ordering (sec-ch-ua before accept, as Chrome does)
- β User-Agent (Chrome 137)
- β GREASE values (random per-connection, as Chrome does)
- β Certificate compression (brotli, zlib)
- β ALPN (h2, http/1.1)
What This Means in Practice
| Site | v6.1 (rustls) | v6.2 (BoringSSL) |
|---|---|---|
| Cloudflare Basic | β οΈ Sometimes | β Always passes |
| Cloudflare JS Challenge | β Blocked | β Cookie-based pass |
| DataDome | β Blocked | β Passes (JA3 matches) |
| PerimeterX/HUMAN | β Blocked | β Passes (JA3+H2 match) |
| Akamai Bot Manager | β Blocked | β Passes (H2 FP match) |
| Cloudflare Turnstile | β Needs WebView | β Still needs WebView |
Changes
crates/bex-core/Cargo.toml:
- Removed
reqwestwithrustls-tlsfeature - Added
rquest = { version = "1.0", features = ["full"] } - Version bumped to 2.1.0
crates/bex-core/src/http_service.rs: Complete rewrite:
- Uses
rquest::ClientwithImpersonate::Chrome137 - Removed all manual header construction (
browser_default_headers()deleted) - Headers are now managed by the impersonation layer automatically
- Plugin headers still override when explicitly set (for custom Referer, etc.)
- Auto-Referer from URL origin still works
- Cache logic unchanged
Build Requirements
rquest bundles BoringSSL which requires:
# Linux:
apt-get install cmake clang
# macOS:
brew install cmake llvm
The build handles BoringSSL compilation automatically via the boring crate.
Migration Notes for Plugin Authors
No changes needed. The WIT interface (http::send-request) is unchanged.
Plugins still set their own headers via Request.headers β these override
the Chrome defaults when explicitly provided.
The only behavioral difference: requests that previously got CF-challenged or blocked will now succeed transparently. No code changes needed in plugins.
Known Limitation
Cloudflare Turnstile (interactive CAPTCHA) still requires actual human interaction. This is by design β Turnstile uses browser fingerprinting + proof-of-work that cannot be solved programmatically without a full WebView.