Cloudflare
The cloudflare Cargo feature ships a driver that bypasses Cloudflare's
interactive Turnstile challenge — the "verify you are human"
checkbox page that gates many sites behind a CDN. It is not a generic
anti-Cloudflare solution; it specifically automates clicking the visible
Turnstile checkbox iframe and waiting for the resulting clearance token.
Enable it in Cargo.toml:
[dependencies]
zendriver = { version = "0.1", features = ["cloudflare"] }
The entry point is Tab::cloudflare, which constructs a
CloudflareBypass driver scoped to that tab's session. Call
wait_for_clearance with a timeout to run the full detect-click-poll
flow.
Usage
//! Demonstrates the P5 Cloudflare Turnstile bypass driver. //! //! Sequence: //! 1. Launch a headless browser and navigate to a Cloudflare-protected //! URL. Set this to whatever endpoint you actually want to clear. //! 2. Call [`Tab::cloudflare`] to construct a [`CloudflareBypass`] bound //! to the tab's session. //! 3. Call [`CloudflareBypass::wait_for_clearance`] with a 30s budget. //! The driver runs a single CDP poll loop that, per tick, looks for //! the `cf-turnstile-response` token, the Turnstile challenge //! iframe's bounding box (shadow-DOM aware), and any challenge marker //! on the page. When the interactive iframe is present and we have //! not yet clicked it, the driver dispatches a single raw left-click //! at the canonical (15% x, 50% y) offset (Turnstile checkbox). //! Resolves the first tick a token is observed or the iframe is gone //! after a click. //! 4. Print the [`ClearanceOutcome`] enum variant + the page title. //! //! Outcomes: //! - `TokenAcquired(_)` — Turnstile yielded a `cf-turnstile-response` //! token, either after clicking the checkbox or directly (invisible //! Turnstile, where the iframe never mounts). //! - `ChallengeGone` — the interactive iframe was clicked and the //! challenge container then disappeared without a token (e.g. //! clearance-cookie shortcut). //! - `Err(NoChallenge)` — the full timeout window elapsed without any //! challenge markers ever being observed (no container, no hidden //! input, no iframe). The page likely has no Cloudflare gate; not a //! failure. //! - `Err(ClearanceTimeout)` — 30s elapsed with markers present but //! neither success state observed. //! //! Requires the `cloudflare` cargo feature: //! `cargo run --example cloudflare_bypass --features cloudflare`. use std::time::Duration; use zendriver::{Browser, CloudflareError}; #[tokio::main] #[allow(clippy::result_large_err)] // example boundary; users wrap in their own Error async fn main() -> zendriver::Result<()> { tracing_subscriber::fmt::init(); let browser = Browser::builder().headless(true).launch().await?; let tab = browser.main_tab(); tab.goto("https://nopecha.com/demo/cloudflare").await?; tab.wait_for_load().await?; match tab .cloudflare() .wait_for_clearance(Duration::from_secs(30)) .await { Ok(outcome) => println!("cleared: {outcome:?}"), Err(CloudflareError::NoChallenge) => { println!("no challenge detected (page already cleared or no CF gate present)"); } Err(CloudflareError::ClearanceTimeout) => { println!("clearance timed out within 30s"); } Err(e) => return Err(e.into()), } let title = tab.title().await?; println!("title = {title:?}"); browser.close().await?; Ok(()) }
The driver returns a ClearanceOutcome on success:
TokenAcquired(token)— thecf-turnstile-responseinput picked up a non-empty value. The page can now proceed; the token is also forwarded to Cloudflare server-side on the next request.ChallengeGone— the challenge container disappeared without yielding a token, typically because Cloudflare honored a clearance cookie and short-circuited the gate.
Errors are typed via CloudflareError:
NoChallenge— no Turnstile iframe was detected at call time. Often means the page already cleared you (an existing cookie) or there's no CF gate present. Treat as a no-op, not a failure.ClearanceTimeout— the deadline elapsed without resolution. Usually means Cloudflare escalated to the deeper anti-bot path that this driver doesn't handle.
How it works
The driver runs four stages internally:
- Detect. A shadow-DOM-aware walk of the page's main world looks
for the Turnstile iframe (
<iframe>whosesrcmatches Cloudflare's Turnstile widget origin). It surfaces the bounding box. - Click. A raw
mousedown/mouseupis dispatched at offset(bbox.x + bbox.width * 0.15, bbox.y + bbox.height * 0.5)— the canonical 15%-from-left, 50%-from-top position of the Turnstile checkbox inside the iframe. No Bezier-path motion; Cloudflare wants a real click on a real checkbox. - Poll. Every 500 ms (override via
poll_interval), the driver checks bothcf-turnstile-response(for a non-empty token) and the challenge container (for removal from the DOM). - Return. First condition to fire wins; deadline elapsed →
ClearanceTimeout.
Limitations
This driver only handles the visible interactive Turnstile checkbox. It does not solve:
- Silent / invisible Turnstile (no UI element to click — relies on
passive fingerprinting). For those, stealth alone is your only
defense; pair
StealthProfile::spoofed()with a clean residential IP. - Cloudflare's full Pro / Enterprise managed challenge (which can escalate to image puzzles or even hCaptcha).
- Bot Fight Mode soft blocks that issue 403s without a UI.
- Rate-limit blocks (1015 errors) that don't expose a challenge UI at all.
If the bypass times out, switch to a real browser session, manually inspect the page, and confirm whether the gate is the interactive checkbox flow. If it's not, this driver can't help and you'll need a different strategy (better stealth, rotating residential proxies, or giving up on that target).
Pairing with stealth
Cloudflare's challenge logic checks several signals before deciding
whether to show the visible checkbox or escalate to the silent flow:
TLS JA3 fingerprint, User-Agent, header order, navigator.webdriver,
etc. Out-of-the-box headless Chrome trips most of those, so it tends to
get the harder challenge path — sometimes one this driver can't pass.
Pair the bypass with StealthProfile::spoofed() for the best results:
use zendriver::{Browser, StealthProfile};
let browser = Browser::builder()
.stealth(StealthProfile::spoofed()) // patches navigator.webdriver etc.
.launch()
.await?;
let tab = browser.main_tab();
tab.goto("https://target.example.com").await?;
tab.wait_for_load().await?;
tab.cloudflare()
.wait_for_clearance(std::time::Duration::from_secs(30))
.await?;
spoofed patches the Navigator-prototype tells that Cloudflare also
checks during the protocol-level challenge — together they pass most
consumer-site Cloudflare gates. See Stealth for the
profile tradeoffs.
When to call it
Call wait_for_clearance after the navigation completes but
before any post-challenge code that depends on being past the gate.
The typical sequence:
tab.goto(url).await?;
tab.wait_for_load().await?;
match tab.cloudflare()
.wait_for_clearance(Duration::from_secs(30))
.await
{
Ok(_) => { /* cleared */ }
Err(CloudflareError::NoChallenge) => { /* already cleared, fine */ }
Err(e) => return Err(e.into()),
}
// Now your normal scraping / interaction code.
let data = tab.find().css(".product-grid").one().await?;
NoChallenge is informational, not an error — code should treat it as
success. The other variants of CloudflareError should propagate.
Tuning
.poll_interval(Duration::from_millis(200))— tighter polling burns more CPU but reacts faster to clearance. Defaults to 500 ms which balances responsiveness against load.- Pass a generous
wait_for_clearancetimeout (30-60 s) for the first challenge; subsequent navigations on the sameuser_data_dirare usually cookie-shortcut clears and resolve in <1 s viaChallengeGone.