captchaforge
⚠️ DO NOT USE: UNDER ACTIVE DEVELOPMENT
This crate is not production-ready: broad live-vendor success across real anti-bot sites is not yet benchmarked or guaranteed. The architecture is in place (vendor solvers, retry-loop iframe walking, VLM provider abstraction, real-WAF bench harness) and the two historical blockers below have been resolved or disproven, but a measured success rate on the full real-site set is still pending.
The earlier "0%, cross-origin click never delivers / iframe never attaches" diagnosis was empirically DISPROVEN. A deterministic oracle (
foxdriver/tests/cross_origin_click.rs, 3/3 live) proves a trusted top-context click routes into a doubly-nested cross-origin OOPIF (the Turnstile/hCaptcha/reCAPTCHA shape) and arrivesisTrusted = true, and that BiDiinput.performActionsis accepted inside a child-iframe context. The real bug was synthetic JS clicks (el.click(),isTrusted = false) at scored controls, every solver path now locates-in-JS and clicks trusted in Rust.
Real-WAF fixture Status Cloudflare Turnstile, managed (CF demo) ✅ auto-passes with reynard (token obtained, no click needed) Cross-origin trusted click delivery (nested OOPIF) ✅ proven ( cross_origin_click: trusted positives + anisTrusted=falsenegative)Cloudflare Turnstile, always-block ( 2x…AB)✅ INTEGRITY, refuses to fake-pass Cloudflare Turnstile, force-interactive ( 3x…FF)⚪ DEAD ORACLE, differentially proven that no Firefox (stock or reynard) builds the challenge iframe for this test key on a self-hosted page; there is nothing to click The deeper wall was always fingerprint (TLS JA3/JA4 + automation coherence), not the click. That is now substantially addressed: the reynard stealth engine (
navigator.webdriver = falsenatively, FF-150-exact TLS) is the default launch engine, and a persona coherence gate is enforced JS-to-TCP at every launch. Broad live-vendor proof across real sites is the remaining open work.Do not depend on this for any real workload. The repo is private. Reach out to the Santh team if you have a use case that justifies early collaboration.
Automatic CAPTCHA detection and multi-strategy solving for Firefox/BiDi
live pages (runtime_foxdriver::Page, re-exported as
captchaforge::Page). 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:
[]
= { = "https://github.com/santhreal/captchaforge", = "main" }
= { = "1", = ["full"] }
CLI
A standalone binary ships with the workspace:
The serve mode exposes:
GET /doctor: capabilities JSON + install hints (works headless)POST /solve:{"url": "https://...", "headless": true}→ solve resultPOST /transcribe:{"audio_b64": "..."}→{"raw","normalized"}
Use serve to embed solving in any language that speaks HTTP.
Meridian should not use the standalone solve/serve browser path for
auth-preserving hunting; it calls CaptchaForge inside the existing Guise
Firefox/BiDi page through guise-bridge.
Library
The shortest path from "fresh page" to "solved captcha" is one call
[captchaforge::solve_url] wires stealth + named profile before
navigation, warms the loaded page, then runs auto_solve:
use *;
async
If you already navigated and just want the detect-then-solve loop on
the current page, use [captchaforge::auto_solve]:
async
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 Arc;
use ;
let cache = new; // 60s TTL by default
let chain = default_chain
.with_token_cache; // hit cache before solving
let info = detect.await?;
if is_captcha
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 |
| 15 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 legacy diagnostic via PatchedChromiumDetector |
| Per-solver telemetry + Prometheus exposition | telemetry |
chain.with_telemetry(...) + serve /metrics |
| Network-layer WAF challenge replay (no browser) | crate captchaforge-challenge |
direct dep |
| Pure-data rule schema (no browser 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 undersrc/detect/. Add a new detector by writingsrc/detect/<vendor>.rsand registering it.detect::rules, community-extensible TOML rule layer. Drop a[[provider]]entry inrules/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 assolver: "TokenCache".telemetry::SolverTelemetry, implement to capture every solver attempt as aSolveEvent. Default isNoopTelemetry. Production deployments plug in Prometheus / OTel / SQLite / Sentry.solver::PatternStore(per-domain learned routing).save_to_path/load_from_path/mergefor 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-clion PATH,OPENAI_API_KEYenv,tesseracton PATH. Returns aCapabilitiesstruct withinstall_hints()for any missing backend so consumers can tell their users exactly what to install.captchaforge::backends::Capabilities::ensure_vlm_model()spawnsollama pull llama3.2-vision:11bwhen 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.
- Token cache: if a
TokenCacheis installed and holds a fresh(domain, captcha_type)token (default 60s TTL), return it immediately tagged asCrowdSourced. Telemetry sees this as aSuccesswithsolver: "TokenCache". Skip the rest of the chain. - Provider routing: for
DetectedCaptcha::Custom(TOML rule layer), only solvers whoseSolveMethodappears in the bundled provider'srecommended_solver_methodsare eligible, in declared order. Built-in detectors fall back to the standardsupports()filter. - 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). - 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 intoCaptchaSolveResult::cookiesfor replay. - Vision-LLM: screenshot the challenge, hand it to a multimodal
model (Ollama
qwen3-vl:30bout of the box, configurable), parse the model's grid / click answer, simulate the mouse. Captures session cookies on success. - 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.
- Telemetry: every attempt at every layer fires a
SolveEventto the installedSolverTelemetryimpl (defaultNoopTelemetry). Outcomes:Success,Failure,Error,Timeout. Cache hits surface asSuccesswithsolver: "TokenCache"so dashboards distinguish solved-from-scratch from cache-served. - Human fallback: when every solver fails, return unsolved
with a base64 JPEG screenshot in
CaptchaSolveResult::screenshotso 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.
= 60000
[]
= 90
[]
= "http://gpu-host:11434"
[]
= "cap_solver"
= "..."
Builder methods (fine-grained)
For fine-grained control inside one process, use the builder methods they always beat env + TOML.
use ;
let mut chain = empty;
chain.add_solver;
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 ;
let mut chain = empty;
chain.add_solver;
CLI
Telemetry
Two batteries-included impls plus the trait for custom sinks.
use Arc;
use CaptchaSolverChain;
use ;
// 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 = default_chain
.with_telemetry;
// Option 2: in-process Prometheus-shaped aggregator. `serve` wires
// this into `/metrics` and `/metrics.json` automatically.
let metrics = new;
let chain = default_chain
.with_telemetry;
println!;
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 Arc;
use CaptchaSolverChain;
use TrainingCorpus;
let corpus = new;
let chain = default_chain
.with_training_corpus;
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 Duration;
use HotReloadRegistry;
let watcher = start?;
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 browser round-trip
takes, no GPU, no browser 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
# positive HTML fixture, rejects its negative,
# and doesn't cross-fire on other vendors' negatives
# actually reachable from the chain (oracle,
# corpus, hot-reload, telemetry, planner)
# token-oracle invariant (decoy never
# Plausible, classify deterministic, etc.)
Offline module micro-bench
In-process measurements (no browser) 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 live-browser 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)
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 browser binary or a real
Ollama server, browser-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 a live browser runtime is available.
License
MIT OR Apache-2.0 at your option. Copyright 2026 CORUM COLLECTIVE LLC.
Scorecard badges
These measure structural coverage, that the solver chain, the community rule set, the challenge-fingerprint catalogue, and the test/property suites are complete and wired. They are not a live-vendor success rate: a measured pass rate against real anti-bot sites is still open work (see the banner at the top). A 100% here means "every stage/fixture we model is exercised", not "we beat every real CAPTCHA".