captchaforge 0.2.30

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation

captchaforge

ci license

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers — Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio challenges, sliders, plus a community-extensible TOML rule pack for vendors like AWS WAF Captcha, DataDome, Akamai Bot Manager, PerimeterX/HUMAN, Arkose/FunCaptcha, Geetest v3+v4, Friendly Captcha, MTCaptcha, and WordPress math captchas.

Part of the Santh security research ecosystem. Beta — used in production by wafrift and golemn-browser but the public API may shift before 1.0. Pin the version in Cargo.toml.

Quick start

Add to Cargo.toml:

[dependencies]
captchaforge = { git = "https://github.com/santhsecurity/captchaforge", branch = "main" }
chromiumoxide = "0.9"
tokio = { version = "1", features = ["full"] }

CLI

A standalone binary ships with the workspace:

cargo install --path cli                       # builds `captchaforge`
captchaforge doctor                            # probe Ollama / Whisper / Tesseract
captchaforge setup --auto                      # auto-pull default vision model
captchaforge selftest                          # confirm the 11-solver chain wires up
captchaforge transcribe sample.wav             # smoke-test the STT backend
captchaforge solve https://example.com         # solve a captcha on a real URL
captchaforge serve --port 8080                 # HTTP API for non-Rust embeds

The serve mode exposes:

  • GET /doctor — capabilities JSON + install hints (works headless)
  • POST /solve{"url": "https://...", "headless": true} → solve result
  • POST /transcribe{"audio_b64": "..."}{"raw","normalized"}

Use serve to embed solving in any language that speaks HTTP.

Library

The shortest path from "fresh page" to "solved captcha" is one call — [captchaforge::solve_url] wires stealth + named profile + warmup before the navigation, then runs auto_solve after:

use captchaforge::prelude::*;

async fn open(page: &chromiumoxide::Page) -> anyhow::Result<()> {
    match solve_url(page, "https://example.com",
                    Some(StealthProfile::ChromeMacStable)).await? {
        None                          => println!("no captcha"),
        Some(r) if r.success          => println!("solved via {:?} ({}ms)", r.method, r.time_ms),
        Some(r)                       => println!("failed: {:?}", r.method),
    }
    Ok(())
}

If you already navigated and just want the detect-then-solve loop on the current page, use [captchaforge::auto_solve]:

async fn handle_captcha(page: &chromiumoxide::Page) -> anyhow::Result<()> {
    // Returns Ok(None) for no-captcha pages.
    if let Some(outcome) = captchaforge::auto_solve(page).await? {
        if outcome.success {
            tracing::info!(method = ?outcome.method, "captcha solved");
        } else if let Some(screenshot) = outcome.screenshot {
            tracing::warn!(bytes = screenshot.len(), "human review needed");
        }
    }
    Ok(())
}

For high-stakes flows (checkout, account creation, bot-blocked public APIs) wrap the call in [captchaforge::auto_solve_with_retries] — it dismisses cookie-consent banners between attempts and retries on transient failure. Pair with [captchaforge::dismiss_chat_widget] when an Intercom / Drift / HubSpot launcher overlaps the captcha widget.

For full control — telemetry, token cache, custom solvers — drop down to the explicit chain:

use std::sync::Arc;
use captchaforge::{detect, solver::{CaptchaSolverChain, TokenCache}};

let cache = Arc::new(TokenCache::new());     // 60s TTL by default
let chain = CaptchaSolverChain::default_chain()
    .with_token_cache(cache);                 // hit cache before solving

let info = detect::detect(page).await?;
if detect::is_captcha(&info) {
    let outcome = chain.solve(page, &info).await;
    // ...
}

Capability map

Each capability has its own module + at least one production wiring point. Re-export paths are at the crate root; the internal home is one of captchaforge::*.

Capability Module Wired into
One-call solve / prepare / dismiss helpers sdk captchaforge::{solve_url, …} re-exports
158-vendor TOML rule pack detect::rules ProviderRegistry::with_built_in_rules
12 named browser stealth profiles stealth_profiles prepare_page(_, Some(profile))
Real-human mouse-trace sampler (8-trace corpus + jitter) mouse_sampler opt-in; behavior::mouse_move_human ships the legacy path
Bezier mouse / hover-dwell / touch-swipe / pointer-jitter / triple-click behavior bench-suite + production solvers
Audio captcha DSP (denoise + bandpass + normalise) audio_dsp AudioCaptchaSolver::transcribe
DOM/frame graph (BFS / deepest-captcha / ancestors) frame_graph opt-in for solver authors
Multi-hop captcha planner solver::planner opt-in via MultiHopPlanner
Read-only artifact pre-flight handle solver::speculation opt-in via PreflightHandle
Per-vendor token shape oracle solver::token_shapes chain post-solve check (auto)
Adversarial training corpus training_corpus chain.with_training_corpus(...)
VLM training dataset (content-addressed) vlm_dataset training pipeline (training/)
Mouse-trace ingest store trace_ingest server side of B1
Vendor JS scraper + probe-surface diff vendor_scraper captchaforge scrape-vendors CLI
TOML rule hot-reload (mtime-poll, atomic swap) rule_watcher opt-in via HotReloadRegistry::start
Patched-chromium sidecar detector chromium_sidecar opt-in via PatchedChromiumDetector
Per-solver telemetry + Prometheus exposition telemetry chain.with_telemetry(...) + serve /metrics
Network-layer WAF challenge replay (no chromium) crate captchaforge-challenge direct dep
Pure-data rule schema (no chromium dep) crate captchaforge-rules direct dep for static-analysis tools

Architecture surface (post-extraction)

  • detect::Detector — single per-vendor detector trait. Eleven built-ins (TurnstileDetector, RecaptchaDetector, …) live one per file under src/detect/. Add a new detector by writing src/detect/<vendor>.rs and registering it.
  • detect::rules — community-extensible TOML rule layer. Drop a [[provider]] entry in rules/community.toml (or your own file) and the vendor is detected without writing any Rust. Bundled pack covers DataDome, Akamai, PerimeterX/HUMAN, Arkose/FunCaptcha, Geetest v3+v4, AWS WAF Captcha, Friendly Captcha, MTCaptcha, WordPress math captchas.
  • provider::CaptchaProvider — bundles a detector + the recommended solver methods for that vendor in one trait. Built-in providers wired via macro; new providers implement both traits in one file.
  • provider::ProviderRegistry::with_built_in_rules() — single call returning the built-in detectors PLUS the bundled community rule pack, all priority-sorted.
  • solver::CaptchaSolverChain — multi-strategy chain with cache
    • telemetry + pattern-store routing. See "Strategies" below.
  • solver::TokenCache — per-(domain, captcha_type) solved token cache with configurable TTL. Cache hits skip the solver run entirely and report through telemetry as solver: "TokenCache".
  • telemetry::SolverTelemetry — implement to capture every solver attempt as a SolveEvent. Default is NoopTelemetry. Production deployments plug in Prometheus / OTel / SQLite / Sentry.
  • solver::PatternStore — per-domain learned routing. save_to_path / load_from_path / merge for cross-restart and cross-instance sharing.
  • captchaforge::auto_solve() — top-level one-call API for the 90% case. Auto-detects local backends (Ollama VLM / Whisper / Tesseract) on first call and logs the capability summary so operators see what's wired without reading the source.
  • captchaforge::backends::probe() — non-blocking parallel probe of Ollama (/api/tags), whisper / whisper-cli on PATH, OPENAI_API_KEY env, tesseract on PATH. Returns a Capabilities struct with install_hints() for any missing backend so consumers can tell their users exactly what to install.
  • captchaforge::backends::Capabilities::ensure_vlm_model() — spawns ollama pull llama3.2-vision:11b when Ollama is reachable but no vision model is present. Auto-install for the SDK consumer.

Strategies

For each detected captcha the chain runs through a layered short circuit + strategy stack. First match wins.

  1. Token cache — if a TokenCache is installed and holds a fresh (domain, captcha_type) token (default 60s TTL), return it immediately tagged as CrowdSourced. Telemetry sees this as a Success with solver: "TokenCache". Skip the rest of the chain.
  2. Provider routing — for DetectedCaptcha::Custom (TOML rule layer), only solvers whose SolveMethod appears in the bundled provider's recommended_solver_methods are eligible, in declared order. Built-in detectors fall back to the standard supports() filter.
  3. Pattern store ordering — for built-in kinds, solvers eligible under (1) get re-ordered so the historically-best method for this domain runs first. PatternStore can be persisted across restarts (save_to_path / load_from_path) and merged across worker instances (merge).
  4. Behavioural — realistic mouse movement + timing for Turnstile and reCAPTCHA v3. Solvers verify the response token is actually present before reporting success. On success, captures session cookies into CaptchaSolveResult::cookies for replay.
  5. Vision-LLM — screenshot the challenge, hand it to a multimodal model (Ollama qwen3-vl:30b out of the box, configurable), parse the model's grid / click answer, simulate the mouse. Captures session cookies on success.
  6. Audio bypass — click the accessibility audio challenge, run STT, type the transcribed answer, then verify the reCAPTCHA token was accepted. Captures session cookies on success.
  7. Telemetry — every attempt at every layer fires a SolveEvent to the installed SolverTelemetry impl (default NoopTelemetry). Outcomes: Success, Failure, Error, Timeout. Cache hits surface as Success with solver: "TokenCache" so dashboards distinguish solved-from-scratch from cache-served.
  8. Human fallback — when every solver fails, return unsolved with a base64 JPEG screenshot in CaptchaSolveResult::screenshot so a downstream human-in-the-loop can finish.

Apply captured cookies to subsequent navigations via captchaforge::cookies::apply_to_page(&page, &result.cookies) so the WAF/vendor's trusted session is preserved and the next request does not re-trigger the captcha.

You can plug in your own solver by implementing the CaptchaSolver trait and adding it to the chain via chain.add_solver(...) before default_chain() strategies.

Detection coverage

Built-in detectors (one per file under src/detect/):

  • Cloudflare Turnstile (.cf-turnstile, [data-turnstile-sitekey], script[src*="challenges.cloudflare.com"])
  • Cloudflare bot interstitial ("Just a moment", #challenge-form)
  • reCAPTCHA v2 + v3 (.g-recaptcha, [data-sitekey], script[src*="google.com/recaptcha"])
  • hCaptcha (.h-captcha, [data-hcaptcha-sitekey], script[src*="hcaptcha.com"])
  • Generic image / audio CAPTCHAs
  • PoW CAPTCHAs (ALTCHA, mCaptcha, Friendly Captcha, Cap.dev)
  • Slider / swipe / rotate / drag CAPTCHAs
  • Canvas / SVG / math / icon / color CAPTCHAs
  • Multi-step / wizard CAPTCHAs
  • Shadow-DOM-hidden CAPTCHAs

Bundled community rule pack (rules/community.toml, registered via ProviderRegistry::with_built_in_rules()):

  • AWS WAF Captcha
  • DataDome
  • Akamai Bot Manager
  • PerimeterX / HUMAN
  • Arkose Labs / FunCaptcha
  • Geetest v3 + v4
  • Friendly Captcha
  • MTCaptcha
  • WordPress math captchas (Captcha by BestWebSoft, generic forms)

Adding a new family is two lines of TOML — see rules/community.toml for the schema. Per-rule positive + negative HTML fixtures live under tests/rule_fixtures/<vendor>/; the community_rules integration test asserts every rule fires on its positive and rejects its negative AND doesn't cross-fire on any other vendor's negative.

Configuration

Two layers, lightest to heaviest:

Env vars (zero-config)

  • CAPTCHAFORGE_VLM_ENDPOINT — Ollama endpoint URL.
  • CAPTCHAFORGE_VLM_MODEL — VLM model name.
  • CAPTCHAFORGE_THIRDPARTY_API_KEY — 2captcha-compatible service key.
  • CAPTCHAFORGE_THIRDPARTY_ENDPOINT — override the service base URL.
  • CAPTCHAFORGE_CONFIG — explicit Tier-A config file path.

Tier-A operational config (.captchaforge.toml)

Drop .captchaforge.toml in your project (or use the discovery search path: $CAPTCHAFORGE_CONFIG, ./.captchaforge.toml, ./captchaforge.toml, $XDG_CONFIG_HOME/captchaforge/config.toml). The schema covers chain timeouts, cache TTL, VLM endpoint/model, third-party service selection, and every SolveConfig field. See .captchaforge.toml.example for the reference. Unknown keys are a parse error so typos surface instead of silently being ignored.

per_solver_timeout_ms = 60000

[cache]
ttl_seconds = 90

[vlm]
endpoint = "http://gpu-host:11434"

[third_party]
service = "cap_solver"
api_key = "..."

Builder methods (fine-grained)

For fine-grained control inside one process, use the builder methods — they always beat env + TOML.

use captchaforge::solver::{VlmCaptchaSolver, CaptchaSolverChain};

let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
    VlmCaptchaSolver::new()
        .with_endpoint("http://my-gpu-host:11434")
        .with_model("qwen3-vl:30b"),
);

Audio STT endpoint is similarly configurable on AudioCaptchaSolver.

Third-party solver service

ThirdPartyCaptchaSolver talks the 2captcha-compatible HTTP protocol used by 2captcha, CapMonster, and CapSolver — every TOML-bundled vendor that recommends ThirdPartyService now actually has a solver to land on. Without an API key configured, supports() returns false across the board so the solver is skipped instead of issuing 401s.

use captchaforge::solver::{ThirdPartyCaptchaSolver, CaptchaSolverChain};

let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
    // Pick the service flavour, then pass the key:
    ThirdPartyCaptchaSolver::cap_solver().with_api_key("..."),
);

CLI

cargo install --path cli
captchaforge inspect-rules                         # list bundled + extra rules
captchaforge detect https://example.com/login      # navigate, run detector
captchaforge solve  https://example.com/login      # navigate, run full chain
captchaforge inspect-rules --rules my-pack.toml    # additive extra rules
captchaforge rules                                 # 158-vendor coverage table
captchaforge profiles                              # 12 named StealthProfiles
captchaforge corpus stats /var/lib/cf/corpus       # adversarial-training samples
captchaforge dataset stats /var/lib/cf/datasets    # VlmDataset shape
captchaforge scrape-vendors --out report.txt       # diff vendor JS probe surface
captchaforge validate-rules                        # static-check every TOML rule
captchaforge serve --port 8080                     # HTTP API (incl. /metrics)

Telemetry

Two batteries-included impls plus the trait for custom sinks.

use std::sync::Arc;
use captchaforge::solver::CaptchaSolverChain;
use captchaforge::telemetry::{JsonTelemetry, MetricsTelemetry};

// Option 1: route every event through `tracing::info!` with structured
// fields. Pairs with `tracing-subscriber`'s JSON layer for ELK / Loki
// / Datadog without any extra integration code.
let chain = CaptchaSolverChain::default_chain()
    .with_telemetry(Arc::new(JsonTelemetry));

// Option 2: in-process Prometheus-shaped aggregator. `serve` wires
// this into `/metrics` and `/metrics.json` automatically.
let metrics = Arc::new(MetricsTelemetry::new());
let chain = CaptchaSolverChain::default_chain()
    .with_telemetry(metrics.clone());
println!("{}", metrics.snapshot().to_prometheus());

The chain fires one event per solver attempt at every outcome (Success / Failure / Error / Timeout). Cache hits also fire as Success with solver: "TokenCache" so dashboards distinguish solved-from-scratch from cache-served.

Adversarial training loop

Every failed solve auto-appends to a TrainingCorpus when one is configured. After ~1 week of production traffic you have a labelled- by-failure dataset ready to feed back into a fine-tune.

use std::sync::Arc;
use captchaforge::solver::CaptchaSolverChain;
use captchaforge::training_corpus::TrainingCorpus;

let corpus = Arc::new(TrainingCorpus::open("/var/lib/captchaforge/corpus")?);
let chain = CaptchaSolverChain::default_chain()
    .with_training_corpus(corpus.clone());

The Python compute side lives in training/: Dockerfile + train_lora.py (LoRA fine-tune of Qwen2-VL-7B on a captchaforge VlmDataset), distill.py (7B teacher → 2B student distillation), auto_label.py (GPT-4V / Claude vision teacher labelling), bench_compare.py (CI gate for model promotion based on per-vendor regression bound), promote_to_ollama.sh (HF → GGUF → Ollama registration).

Hot-reload TOML rules

Operators drop a new vendor rule into the watched dir; the registry atomically swaps in <2s without a process restart.

use std::time::Duration;
use captchaforge::rule_watcher::HotReloadRegistry;

let watcher = HotReloadRegistry::start("/etc/captchaforge/rules.d", Duration::from_secs(2))?;
let registry = watcher.current(); // Arc<ProviderRegistry> — atomically swapped on TOML change

Bad TOML keeps the previous registry active and logs at warn (no restart spam). The validate-rules CLI subcommand pre-flights every rule before promotion.

Network-layer challenge replay

For WAF challenges that are JS-only (no visual captcha), the captchaforge-challenge workspace crate solves them in pure HTTP — typically 50-150ms instead of the 8-12s a chromium round-trip takes, no GPU, no chromium binary. Cloudflare interstitial reference impl ships today; DataDome / Akamai / Imperva / Kasada slot into the same ChallengeSolver trait as their JS is reverse-engineered.

Origin and licensing

Extracted from golemn-browser (originally GPL-3.0). The original author (Santh Project) re-licensed the extracted slice as MIT OR Apache-2.0 so it can be embedded in MIT/Apache-licensed downstream tools (notably wafrift for managed-WAF challenge handling and any future Santh tool that needs a captcha layer).

Testing

cargo test --workspace        # unit + integration (no chromium required)
cargo test --doc              # doctest in lib.rs
cargo test --test community_rules    # asserts every TOML rule fires on its
                              # positive HTML fixture, rejects its negative,
                              # and doesn't cross-fire on other vendors' negatives
cargo test --test integration_chain_wires  # cache + telemetry + chain wires
cargo test --test wirings    # production wirings: H-series modules
                             # actually reachable from the chain (oracle,
                             # corpus, hot-reload, telemetry, planner)
cargo test --test property_token_shapes   # ~10k randomised cases per
                             # token-oracle invariant (decoy never
                             # Plausible, classify deterministic, etc.)

Offline module micro-bench

cd bench
cargo run --release -- --suite modules

In-process measurements (no chrome) of the H-series hot paths:

  • token-oracle classify (1k iters)
  • token-oracle resolve (1k)
  • frame-graph construction (1k)
  • mouse-sampler trace synthesis (1k)
  • training-corpus append (100)

Total runtime <1s. Output uses the same regression-delta tooling as the chrome suites, so per-module slowdowns surface in the bench-baseline.json diff. CI runs this on every PR via the --suite quick aggregate.

Local proof (benches)

cargo run --example architecture_proof --release

Prints a markdown report covering the latency-critical paths added by the architecture sweep:

  • TokenCache get hit / miss
  • ProviderRegistry::find_by_kind for Custom kinds
  • RuleDetector probe generation (one-shot at registration)
  • Per-rule HTML fixture match latency (one row per bundled vendor)

Browser-free, runs in well under a second after the release build. Use to sanity-check that no future change regresses the hot path.

The test suite does not require a real chromium binary or a real Ollama server — all chromium-bound paths are pure Rust unit tests against the JavaScript / Rust glue layer. Tests that exercise the real headless browser live in wafrift-captchaforge-bridge where the chromium runtime is available.

License

MIT OR Apache-2.0 at your option. Copyright 2026 CORUM COLLECTIVE LLC.