BrowserOxide — Headless Stealth Browser with Rust Core
A from-scratch stealth browser engine in Rust — own BoringSSL TLS/JA4 fingerprint, real JS, no Chromium, no CDP. Python + MCP bindings.
BrowserOxide is a stealth headless browser engine written from scratch in Rust for web scraping, archival, and AI agents. It implements a real HTML/CSS/DOM/JS browser — including its own BoringSSL TLS stack and a native (not injected) browser fingerprint — with no Chromium and no Chrome DevTools Protocol (CDP) driver underneath. You control every surface from the TLS handshake through WASM to canvas, so the fingerprint is native rather than injected.
TL;DR. Every other native-fingerprint stealth browser is either Python-only (camoufox, a Firefox fork) or drives Chrome over CDP (nodriver, Obscura), where the automation is itself detectable. As of 2026 there is no Rust equivalent to camoufox — BrowserOxide is built to be exactly that. In a same-machine, same-IP cleanroom run it routed 118 of 126 commercially-protected sites (Cloudflare, Akamai, DataDome, PerimeterX, Kasada) to a real render with zero per-vendor bypass code. Kasada is the one honest open gap. And because it's a single Rust process with no Chromium underneath, it renders pages in tens of MB — roughly 15× lighter than a headless-Chrome process tree (see Memory).
Get started: Rust · Python (pip install browser-oxide) · MCP for AI agents · Benchmark · How it compares
Status: research-grade, pre-1.0. API surfaces are not stable. License is MIT OR Apache-2.0.
Why this exists
Most stealth tooling today wraps a real browser and hides the puppet strings.
CDP-driver stealth plugins patch a handful of JS properties at runtime and
lose to Function.prototype.toString checks. Patched-Chromium forks still
inherit Chromium's CDP detection vectors. Patched-Firefox forks ride a
browser engine with low market share, which is itself a fingerprint signal.
BrowserOxide is a different bet: build the engine from the parser up so the fingerprint properties are native, not injected. There is no Chrome process, no CDP client, no WebDriver, no patched-fork inheritance. Whether that's the right bet is empirical — see the numbers section.
What it is
A complete browser engine for scraping, archival, and AI agent workloads:
- V8 JavaScript via
deno_core0.403 — full ES2024+, WASM, JIT - Arena-allocated DOM with
NodeId(Copy, u32) handles, Shadow DOM, iframes - Own CSS engine — Syntax L3 tokenizer, Selectors L4 matcher, cascade,
@layer/@media/@container, computed styles - Real Canvas — 2D rendering via
tiny-skia, WebGL stubs, AudioContext - Layout via
taffywith font metrics forgetBoundingClientRect() - Stealth HTTP — own TLS stack (
boring2/BoringSSL), Chrome-matched ClientHello, HTTP/1.1 + HTTP/2 (HTTP/3 wired but disabled by default — vanillaquinn-protoemits randomized transport parameters which is a worse fingerprint than not speaking h3) - CDP-compatible debugging surface — drop-in target for Puppeteer/ Playwright via WebSocket
- EventSource (SSE) for streaming endpoints
- Configurable browser identity — load profiles from YAML or JSON at
runtime, or use the built-in
chrome_148_*/firefox_135_*/pixel_9_pro_chrome_148/iphone_15_pro_safari_18presets
How it compares
The native-fingerprint stealth-browser field, and where BrowserOxide sits:
BrowserOxide is the only one built from scratch — a Rust/Python engine with no Chromium, no Chrome/CDP, and no browser fork underneath. Every other tool is either a fork (camoufox = Firefox), a Chrome driver (nodriver, Obscura, undetected-chromedriver), or not a browser at all (curl_cffi).
| Tool | Language | Engine | No CDP/WebDriver | Native fingerprint | Runs JS | TLS impersonation |
|---|---|---|---|---|---|---|
| BrowserOxide | Rust/Python | from-scratch | ✅ | ✅ | ✅ (V8) | ✅ (JA3/JA4) |
| camoufox | Python | Firefox fork | ✅ | ✅ (patched into Firefox) | ✅ (Gecko) | partial (Firefox NSS) |
| nodriver / Obscura | Python | real Chrome (CDP) | ❌ (uses CDP) | ❌ (injected) | ✅ (Chrome) | ❌ (real Chrome TLS) |
| undetected-chromedriver | Python | real Chrome (WebDriver) | ❌ (WebDriver) | ❌ (injected) | ✅ (Chrome) | ❌ |
| curl_cffi | Python | none (HTTP/TLS only) | n/a | n/a | ❌ | ✅ (JA3/JA4) |
The takeaway: camoufox proved native-fingerprint stealth works, but it's a Python-only Firefox fork. CDP-driven tools (nodriver, Obscura) inherit Chrome's automation-detection surface. curl_cffi nails the TLS handshake but runs no JavaScript, so it loses to any JS challenge. As of 2026 there is no Rust equivalent to camoufox — BrowserOxide fills that gap: native fingerprint, no CDP, real JS, and a Rust core with Python + MCP bindings.
What it can do (measured, not estimated)
Anti-bot coverage measured against a 126-site corpus of commercially-
protected pages (Cloudflare, Akamai, DataDome, PerimeterX, Kasada,
Shape/F5, etc.), release build. These numbers are from the
vendor-stripped open-source engine — no per-vendor bypass code in the
tree. Same machine, same IP, same hour, same classifier
(browser_oxide::engine_classify).
| BrowserOxide profile | Pass (real render, ≥15 KB) | loose L3 tag |
|---|---|---|
chrome_148_macos |
114 | 118 |
firefox_135_macos |
111 | 115 |
pixel_9_pro_chrome_148 |
114 | 118 |
iphone_15_pro_safari_18 |
118 | 121 |
| best-of-4 routed | 118 | 121 |
The headline Pass column is the honest gate: the engine's L3-RENDERED
tag and ≥15 KB of actual content (ChallengeVerdict::Pass, the rule
the engine's own audit harness uses). The loose column counts the
L3-RENDERED tag alone — it over-counts by ~4, because the corpus has
10–15 SPA-bootstrap sites that ship a 2–13 KB shell to any HTTP client
and get tagged L3-RENDERED by the absence of a challenge marker even
though no real render happened (e.g. duolingo returns a 13.5 KB shell —
not a pass). "Routed" = the caller picks the best profile per domain,
which most real scraping pipelines do naturally. Full breakdown and the
scoring caveats (the strict gate also has a few false negatives) are in
docs/BENCHMARK.md.
(Profile labels reflect the actual emitted User-Agent. All presets ship a current Chrome 148 / Firefox 135 / Safari 18 identity.)
The hard residual is seven sites that returned no real content on any
profile: three Kasada pages (canadagoose.com, hyatt.com,
realtor.com — no OSS tool publicly passes Kasada from scratch),
etsy.com (DataDome interactive Device-Check, human-gated, out of
scope), duolingo.com (CSR SPA that only reaches its shell),
wildberries.ru (WBAAS), and homedepot.com (Akamai sec-cpt — flaky;
renders on a good risk-roll). adidas.com is not in this set — it
passes via routing (chrome + iphone render the 1.5 MB storefront; firefox
- pixel stay at the interstitial).
adidas/homedepotare both flaky Akamai, so the exact residual shifts ±1–2 sites run to run.
The engine carries the number, not bypass code. A/B measurements with per-vendor challenge solvers enabled vs fully removed from the tree show no difference in routed pass rate. Every site that renders, renders on the from-scratch TLS + fingerprint + V8 engine alone. The open-source engine ships no solver implementations (see "Challenge solving" below).
Things to know before believing the numbers
- Anti-bot responses are noisy. Single sweep runs vary by ±5 sites
from WAF lottery alone (measured per-site 3× re-tests: amazon variants
have ~1-in-3 pass rate per fetch). The routed
118is the central tendency of one cleanroom run, not a guaranteed per-run result. - Kasada is the OSS-wide gap. No open-source tool publicly passes Kasada from scratch.
Memory: ~15× lighter than headless Chrome
BrowserOxide is a single Rust process — no Chromium, no CDP, no renderer
subprocesses — so it renders a page in tens of MB while a headless Chrome
instance spins up a multi-process tree that runs into the gigabytes. Peak
resident set size (RSS), 5-run median, same box, same pages — BO
single-process VmHWM vs Chrome's full process tree (browser + renderers +
GPU + utility, summed):
| Page | BrowserOxide | headless Chrome 147 | |
|---|---|---|---|
| example.com (528 B) | 49 MB | 1,028 MB | ~21× lighter |
| wikipedia (~230 KB) | 63 MB | 1,111 MB | ~18× lighter |
| x.com (SPA) | 87 MB | 1,302 MB | ~15× lighter |
| nytimes.com | 133 MB | 1,743 MB | ~13× lighter |
Across the full 126-site corpus, BO's median per-page RSS is ~79 MB. This is the practical edge for fleets: where one Chrome wants ~1–2 GB/page, you can run 10–20+ BrowserOxide pages in the same footprint.
Per-page performance
Per-page wall-clock, 5-run median, same box, single IP, warm binary cache:
| Path | example.com (528 B) | hacker news (~35 KB) | wikipedia (~230 KB) |
|---|---|---|---|
cold (Page::navigate) |
244 ms | 444 ms | 849 ms |
warm pool (PagePool::navigate) |
141 ms | 333 ms | 724 ms |
A warm PagePool amortizes V8 isolate + snapshot setup across
navigations (the PagePool::navigate(url) API).
Full per-profile + per-site breakdown: docs/BENCHMARK.md.
FAQ
Is there a Rust equivalent to camoufox?
BrowserOxide is built to be exactly that. camoufox is a native-fingerprint stealth browser, but it's a Python-only Firefox fork. As of 2026 there is no other from-scratch stealth browser engine in Rust — BrowserOxide provides a native fingerprint, no CDP, real JavaScript execution, and a Rust core with Python and MCP bindings.
Is BrowserOxide a Chromium or Firefox fork?
No. The HTML parser, CSS engine, DOM, layout, and TLS stack are written from
scratch in Rust; JavaScript runs on V8 via deno_core. There is no Chrome
process and no CDP driver, so it doesn't inherit Chromium's automation-detection
vectors (navigator.webdriver, cdc_* variables, CDP WebSocket fingerprints).
How is it different from nodriver, Obscura, or undetected-chromedriver?
Those drive a real Chrome over CDP or WebDriver. BrowserOxide is its own engine — no automation protocol underneath — so the fingerprint is native rather than patched onto an automated Chrome that vendors can detect.
How is it different from curl_cffi?
curl_cffi impersonates a browser's TLS handshake but doesn't run JavaScript, so it fails any site needing JS or a real DOM. BrowserOxide ships a full V8 runtime, real DOM/CSS/layout/canvas and a Chrome-matched TLS fingerprint.
Does it pass Cloudflare, Akamai, and DataDome?
In a measured 126-site cleanroom run it routed 118/126 commercially-protected sites to a real render, including most Cloudflare, Akamai, AWS WAF, and DataDome pages — with no per-vendor bypass code. DataDome's interactive Device-Check (human-gated, e.g. etsy.com) is not cleared.
Does it pass Kasada?
No. Kasada (e.g. canadagoose.com, hyatt.com, realtor.com) is the standing open gap; no open-source tool publicly passes Kasada from scratch, and BrowserOxide ships no Kasada solver.
Can I use it from Python?
Yes — pip install browser-oxide, then Browser / Page / Profile / Verdict
(the GIL is released during navigation). There's also an MCP server so AI agents
can drive the engine. See docs/getting-started-python.md.
Does it work with Puppeteer or Playwright?
Yes, via a CDP-compatible WebSocket server (CdpServer::start_navigable) as a
drop-in target — but nothing drives a real browser underneath. There is no
Selenium/WebDriver support.
Is it production-ready?
It's research-grade and pre-1.0; APIs are not stable and the crates aren't on crates.io yet. Anti-bot pass rates are point-in-time and vary ±5 sites per run from WAF noise. MIT OR Apache-2.0.
Challenge solving
The engine exposes a ChallengeSolver trait + a
Page::navigate_with_solvers(url, profile, n, solvers) entry point so
embedders can plug in per-vendor challenge handling (Akamai BMP
sensor_data, Kasada PoW, DataDome interstitial round-trip, Cloudflare
orchestrator). The open-source engine ships no solver
implementations — Page::navigate registers an empty set, so a
challenged page resolves to ChallengeVerdict::ChallengeIncomplete
rather than being auto-cleared. This is deliberate (see SCOPE.md):
site-specific bypass code is out of scope here, and — per the A/B
measurement above — it isn't what produces the corpus pass rate
anyway.
Architecture
HTML → DOM (+ Shadow DOM) → CSS → Layout → JS (V8 + WASM) ← Stealth profile
↑ ↑ ↓
│ iframes Canvas 2D (tiny-skia)
│ ↓
└────── HTTP/1+2 (stealth TLS) ────┘
15 crates, MIT OR Apache-2.0. No GPL/LGPL/AGPL. The only MPL-2.0 in
the default tree is cooked-waker, pulled in transitively via
deno_core → v8 and linked unmodified; MPL-2.0 is file-scope
copyleft so this does not infect downstream code. An optional
blocker Cargo feature (off by default) adds Brave's MPL-2.0
adblock crate. Both are explicit per-crate exceptions in
deny.toml. Full crate inventory in docs/ARCHITECTURE.md.
| Crate | Description |
|---|---|
css_parser |
CSS Syntax Level 3 tokenizer + parser (with nesting) |
css_selectors |
Selectors Level 4 parser + matcher |
css_values |
CSS property value parsing + computed values |
css_cascade |
Cascade, @layer, @media, @container, inheritance |
dom |
Arena DOM + Shadow DOM + iframe contexts |
html_parser |
html5ever integration → DOM |
js_runtime |
V8 (deno_core) + DOM bindings + WASM + Web APIs |
canvas |
Canvas 2D (tiny-skia) + WebGL stubs + AudioContext |
layout |
Box model via taffy (getBoundingClientRect) |
net |
HTTP/1+2+3 + stealth TLS (boring2/BoringSSL) + WebSocket + SSE |
event_loop |
Timers, microtasks, Promises, rAF |
workers |
Web Workers + Service Workers (separate V8 isolates) |
stealth |
Fingerprint profiles (100+ properties), navigator spoofing |
protocol |
CDP server (Puppeteer/Playwright drop-in) |
browser |
Top-level Browser/Page API + ChallengeSolver trait |
Quick start
The engine is !Send (V8 isolates are per-thread), so run it on a current-thread
runtime + LocalSet:
use browser::{ChallengeVerdict, Page};
fn main() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
tokio::task::LocalSet::new().block_on(&rt, async {
let profile = stealth::presets::chrome_148_macos(); // built-in identity
let mut page = Page::navigate("https://example.com", profile, 5).await.unwrap();
println!("{}", page.title());
println!("{}", page.evaluate("document.querySelectorAll('a').length").unwrap());
// challenge pages return HTTP 200 — trust the verdict, not the status:
if page.challenge_verdict() == ChallengeVerdict::Pass {
println!("real content rendered");
}
});
}
Runnable: cargo run --release -p browser_oxide --example getting_started -- https://example.com.
Full walkthrough: docs/getting-started-rust.md.
Configurable browser identity
The browser identity (UA string, Chrome version, screen, locale, TLS
impersonation label, etc.) is a StealthProfile. Load one from disk:
use stealth::StealthProfile;
let profile = StealthProfile::load_from_file("profiles/chrome_148_macos.yaml")?;
profile.validate()?;
let mut page = Page::navigate("https://example.com", profile, 5).await?;
YAML and JSON are both supported; format is picked by extension. See
crates/browser_oxide/profiles/chrome_148_macos.yaml for the full field
schema. The struct definition (StealthProfile in crates/browser_oxide/src/stealth/ profile.rs) is the source of truth — every field is documented there.
Python
from browser_oxide import Browser, Profile, Verdict
with Browser(profile=Profile.chrome()) as b:
page = b.navigate("https://example.com")
print(page.title, len(page.html), page.verdict)
if page.verdict == Verdict.PASS:
print(page.evaluate("navigator.userAgent"))
pip install browser-oxide (or maturin develop from source). Full guide:
docs/getting-started-python.md.
MCP server (for AI agents)
A Model Context Protocol server (browser-oxide-mcp) lets an AI agent drive the
stealth engine — tools: fetch_page, evaluate, and check_protection ("is
this URL behind Akamai/DataDome/Kasada, and did a real render get through?").
{ "mcpServers": { "browser-oxide": { "command": "browser-oxide-mcp" } } }
CDP server (Puppeteer/Playwright drop-in)
use protocol::CdpServer;
let server = CdpServer::start_navigable(9222)?;
// Connect with Puppeteer to ws://127.0.0.1:9222
Build and test
cargo test --workspace -- --test-threads=1 # V8 isolates are per-thread
cargo clippy --workspace -- -D warnings
cargo fmt --all -- --check
# Live anti-bot sweep (needs internet, --release for fair timing)
cargo test --release -p browser_oxide --test holistic_sweep -- --ignored --test-threads=1 --nocapture
# Sweep your own URL list with rich per-site metrics (timing, tag, body len):
cargo run --release -p browser_oxide --example sweep_metrics -- <profile> <corpus.json> <out.json>
<corpus.json> is a JSON array of { "cat", "name", "url" } entries.
Runtime tuning (navigation budgets, the BROWSER_OXIDE_SECCPT_BUDGET_MS
sec-cpt budget, stealth, snapshot, debug) is documented in
docs/CONFIGURATION.md.
The browser-comparison harness (crates/browser_oxide/tests/browser_comparison .rs) compares against locally-installed Chrome / Lightpanda when those
binaries are present; it is #[ignore] by default.
Documentation
Using it
| Guide | Description |
|---|---|
| Getting started (Rust) | Install, navigate, read the page, verdicts, pooling |
| Getting started (Python) | pip install browser-oxide; the Browser/Page/Profile API |
| Profiles | Choosing & customizing browser identities; routing |
| Challenges | Verdict semantics + the ChallengeSolver extension point |
| Stealth FAQ | What's native vs. not — the honest boundary |
| Debugging | Thin renders, fetch logs, JS errors, the probes |
| Configuration | Environment variables — navigation budgets, stealth, snapshot, debug, sweep harness |
| CDP server | Puppeteer/Playwright drop-in |
| Benchmark | Measured anti-bot pass rates |
| Memory | Footprint vs headless Chrome (~15× lighter) + reproduction steps |
Engine internals
| Doc | Description |
|---|---|
| Architecture | Workspace layout, dependency graph, external deps |
| Stealth profiles | Custom browser identity — fields, loading from YAML/JSON, consistency rules |
| Networking | HTTP/1+2+3 + stealth TLS + WebSocket + SSE |
| CDP Protocol | Puppeteer/Playwright drop-in surface |
| CSS Parser / Selectors / Values / Cascade | The CSS engine |
| DOM | Arena DOM + Shadow DOM + iframes + Web APIs |
| JS Runtime | V8 + deno_core + WASM + API surface |
| Canvas | Canvas 2D + WebGL stubs + AudioContext |
| Layout | Box model via taffy |
| Event Loop | Timers, microtasks, rAF, Promises |
| Workers | Dedicated/Shared/Service Workers |
Use, scope, what this is not for
This is engine-side research. The intended use is automated browsing for archival, accessibility, AI agents, security research, and CTF-style challenges where you have legitimate authorization to access the target site. The repository ships an engine, not a "circumvent paywalls" recipe list; site-specific recipes and reverse-engineering notes are kept in a private companion repository.
If you build a product on top of this, respect the target site's terms, robots policy, and rate limits. The maintainer is not responsible for downstream misuse.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. Unless explicitly stated otherwise, any contribution intentionally submitted for inclusion shall be dual-licensed as above, without any additional terms or conditions.
Citation
@software{browseroxide,
title = {BrowserOxide: A From-Scratch Stealth Headless Browser Engine in Rust},
author = {Yury Fedoseev},
year = {2026},
url = {https://github.com/yfedoseev/browser_oxide}
}
Rust + Python + MCP | MIT OR Apache-2.0 | from-scratch stealth headless browser engine — no Chromium, no CDP, no fork | routed 118/126 on a 126-site anti-bot corpus
No comments yet
Be the first to share your take.