captchaforge
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:
[]
= { = "https://github.com/santhsecurity/captchaforge", = "main" }
= "0.9"
= { = "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.
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 *;
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 |
| 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 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 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
# 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 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)
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.
Scorecard badges