# captchaforge
> ## ⚠️ DO NOT USE — UNDER ACTIVE DEVELOPMENT
>
> This crate is **not production-ready**. The architecture is in place
> (vendor solvers, retry-loop iframe walking, VLM provider abstraction,
> real-WAF bench harness with diagnostics) but the **live-vendor
> success rate is still 0%**:
>
> | Real-WAF fixture (iters=1, app.captchaforge.dev origin) | Result |
> |---|---|
> | Cloudflare Turnstile (auto-pass test sitekey 1x...AA) | ❌ 0% — `turnstile.render()` returns a widget_id but iframe is silently never attached |
> | hCaptcha (test sitekey, explicit render+execute) | ❌ 0% — checkbox click fails to deliver into cross-origin anchor frame |
> | reCAPTCHA v2 (Google test sitekey) | ❌ 0% — same cross-origin click delivery gap |
> | Cloudflare Turnstile (always-block 2x...AB) | ✅ 100% — INTEGRITY ✓, no fake-pass |
> | Cloudflare Turnstile (force-interactive 3x...FF) | ❌ 0% — same iframe-no-attach as auto-pass |
>
> Cloudflare detects us at a TLS / CDP fingerprint layer that survives:
> `--headless=new`, removing `--enable-automation`, broadened WebGL
> stealth, real-looking hostname (`app.captchaforge.dev`
> via `--host-resolver-rules`), AND actual non-headless Chrome under
> Xvfb. Defeating it would need TLS-impersonation tier work
> (curl-impersonate-style JA3/JA4 mimicry, residential proxy IPs, or
> running against an allowlisted Cloudflare-customer site) — none of
> which is in this release.
>
> **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.
[](https://github.com/santhsecurity/captchaforge/actions/workflows/ci.yml)
[](#license)
Automatic CAPTCHA detection and multi-strategy solving for
[`chromiumoxide`](https://crates.io/crates/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](https://santh.dev) security research ecosystem.
> **Beta** — used in production by [wafrift](https://github.com/santhsecurity/wafrift)
> and [golemn-browser](https://github.com/santhsecurity/golemn) but the
> public API may shift before 1.0. Pin the version in `Cargo.toml`.
## Quick start
Add to `Cargo.toml`:
```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:
```sh
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:
```rust
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`]:
```rust
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:
```rust
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::*`.
| 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.
0. **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.
1. **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.
2. **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`).
3. **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.
4. **Vision-LLM** — screenshot the challenge, hand it to a multimodal
model ([Ollama](https://ollama.com) `qwen3-vl:30b` out of the box,
configurable), parse the model's grid / click answer, simulate the
mouse. Captures session cookies on success.
5. **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.
6. **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.
7. **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`](src/solver.rs) 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.
```toml
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.
```rust
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.
```rust
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
```sh
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.
```rust
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.
```rust
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/`](./training/README.md):
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.
```rust
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](https://github.com/santhsecurity/golemn)
(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](https://github.com/santhsecurity/wafrift) for managed-WAF
challenge handling and any future Santh tool that needs a captcha
layer).
## Testing
```sh
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
```sh
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)
```sh
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`](https://github.com/santhsecurity/wafrift/tree/main/crates/captchaforge-bridge)
where the chromium runtime is available.
## License
MIT OR Apache-2.0 at your option. Copyright 2026 CORUM COLLECTIVE LLC.
## Scorecard badges





