Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fingerprint spoofing

zendriver-rs ships a first-class fingerprint layer that lets you control what every browser surface reveals to detection scripts — canvas pixel noise, WebGL renderer strings, WebRTC IP candidates, hardware hints, and more — without touching any CDP internals directly.

Two orthogonal axes

Fingerprint control lives on two independent axes:

AxisWhat it controlsWhere it lives
Persona sourceThe identity values injected (UA, platform, WebGL vendor, seed, …)zendriver-stealth (core), or zendriver-fingerprints (pool / generative)
Per-surface render strategyHow each surface is modified in-pageStrategy enum, set per Surface

You can mix any persona source with any per-surface strategy independently.

The 7 surfaces

SurfaceKindDefault strategyWhat it affects
CanvasNoiseSeededgetImageData, toDataURL pixel data
AudioNoiseSeededAnalyserNode frequency / time-domain data
ClientRectsNoiseSeededgetBoundingClientRect sub-pixel dimensions
WebglValueValueUNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL
FontsValueValuemeasureText width noise + FontFaceSet.check allow-list
HardwareValueValueBattery level, media-device count, speech voices
WebrtcPolicyBlockICE candidate leak suppression / fake IP

The 5 strategies

StrategyEffect
NativeNo patch — raw browser output.
SeededDeterministic per-seed noise; same seed → same output every run.
RandomFresh random noise on every call; maximally unpredictable.
BlockEmpty / zero output (appropriate for policy surfaces).
ValueSubstitute a specific value from the Persona spec.

Noise surfaces (Canvas, Audio, ClientRects) accept Native, Seeded, Random, Block. Value surfaces (Webgl, Fonts, Hardware) accept Native, Value, Block. The policy surface (Webrtc) accepts Native, Block, Value (fake IP). Requesting a meaningless combination logs a warning and falls back to the surface's kind default.

Persona sources

Persona::system() — host-probed, cached

Reads the real machine's platform, CPU count, and memory via sysinfo. The result is cached in a OnceLock — first call probes, subsequent calls clone. A random seed is generated per process.

#![allow(unused)]
fn main() {
use zendriver::{Browser, Persona};

let browser = Browser::builder()
    .persona(Persona::system())
    .launch().await?;
}

Persona::builder() — explicit

Build any combination of fields; unset fields inherit from system().

#![allow(unused)]
fn main() {
use zendriver::{Browser, Persona, Seed};

let persona = Persona::builder()
    .seed(Seed::from_u64(42))       // reproducible noise
    .device_memory_gb(16)
    .timezone("America/Los_Angeles")
    .build();

let browser = Browser::builder()
    .persona(persona)
    .launch().await?;
}

Persona::from_browser(tab) — live probe

Read the real browser's values (WebGL renderer, timezone, locale, …) from a running Tab and produce a maximally coherent Persona. Useful when you want to match the identity of an existing browser session.

#![allow(unused)]
fn main() {
use zendriver::{Browser, Persona};

let browser = Browser::builder().launch().await?;
let tab = browser.main_tab();
tab.goto("about:blank").await?;

let persona = Persona::from_browser(tab).await?;
println!("{:?}", persona.webgl);
}

Seed::from_system() — machine-stable seed

Produces the same seed on every run on the same machine (derived from the platform machine ID + hostname). Useful when you want a consistent identity per machine without a user_data_dir.

#![allow(unused)]
fn main() {
use zendriver::{Browser, Persona, Seed};

let persona = Persona::builder()
    .seed(Seed::from_system())
    .build();
}

Pool + generative sources (zendriver-fingerprints)

For real-device personas drawn from a dataset or a Bayesian network, add the optional zendriver-fingerprints crate and enable the pool or generative feature:

[dependencies]
zendriver-fingerprints = { version = "0.1", features = ["pool"] }
#![allow(unused)]
fn main() {
use zendriver_fingerprints::pool::PoolSet;
use zendriver_stealth::Seed;

// Build from a local JSON array (or load with load_or_download(url)).
let pool = PoolSet::from_json(include_str!("pool.json"))?;
let persona = pool.sample(Seed::from_u64(42));

// Pass to Browser::builder() in the zendriver crate.
}

Per-surface strategy overrides

Override any surface's render strategy on top of the persona:

#![allow(unused)]
fn main() {
use zendriver::{Browser, Persona, Seed, Strategy, Surface};

let browser = Browser::builder()
    .persona(Persona::builder().seed(Seed::from_u64(42)).build())
    .surface(Surface::Webrtc, Strategy::Native)  // allow real IP
    .surface(Surface::Canvas, Strategy::Random)  // max entropy
    .launch().await?;
}

Country → locale overlay (geo_locale)

The optional geo feature adds BrowserBuilder::geo_locale, which maps an ISO 3166-1 alpha-2 country code (e.g. "US", "de") to a coherent locale + languages (Accept-Language) set drawn from a bundled CLDR-derived table. It is layered as a persona overlay, so it composes with .persona(..) and is overridden by an explicit .persona_overlay(..) locale. An invalid / unknown country code is ignored (logged) — the value is never locked.

[dependencies]
zendriver = { version = "0.1", features = ["geo"] }
#![allow(unused)]
fn main() {
use zendriver::Browser;

let browser = Browser::builder()
    .geo_locale("DE")   // de-DE locale + matching Accept-Language
    .launch().await?;
}

JSON persona (try_from_json)

Any Persona can be expressed as a JSON object and round-trips cleanly. Fields are snake_case; all fields are optional. Useful for configuration files or environment variables:

#![allow(unused)]
fn main() {
use zendriver::Persona;

let persona: Persona = Persona::try_from_json(r#"{
  "timezone": "Europe/Berlin",
  "device_memory_gb": 8,
  "seed": 12345,
  "webgl": {
    "unmasked_vendor":   "Google Inc. (NVIDIA)",
    "unmasked_renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)"
  },
  "webrtc": { "strategy": "Block" }
}"#).unwrap();
}

You can also parse via FromStr:

#![allow(unused)]
fn main() {
use zendriver::Persona;

let persona: Persona = r#"{"seed": 99, "timezone": "UTC"}"#.parse().unwrap();
}