Stealth profile consistency (#269)

* feat(stealth): randomize browser profile per session

Add a pool of 8 realistic browser profiles (Win10/Win11/macOS, Chrome
143-146). One is picked randomly per BrowserContext instead of always
advertising Linux, which is a strong bot signal.

Each profile keeps navigator.platform, userAgentData.platform, and
userAgentData.platformVersion internally consistent with the UA string.

- profiles.rs: static PROFILES pool + random_profile() picker
- context.rs: pick profile on construction, store platform fields
- page.rs: call rt.set_platform() alongside rt.set_user_agent()
- runtime.rs: add set_platform() to inject __obscura_platform globals
- bootstrap.js: read platform/userAgentData from globals (default Win32)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feat(stealth): match GPU renderer pool to OS profile

Windows profiles get ANGLE Direct3D11 renderers; macOS profiles get
ANGLE Metal renderers. Vendor strings updated to match (Google Inc. (Apple)
for Apple Silicon/Intel Mac, Google Inc. (NVIDIA/AMD/Intel) for Windows).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(stealth): navigator.webdriver returns false instead of undefined

Browsers set navigator.webdriver to false when automation is not active.
Returning undefined is a detectable bot signal — fingerprinting scripts
check typeof navigator.webdriver === 'boolean' as part of headless detection.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* Make stealth profile stable by default, rotation opt-in

Picking a random profile per BrowserContext meant one address would cycle
through different browser identities, which is itself a bot signal, and the
rotated profile carries no matching TLS or timezone fingerprint. Default to a
single stable profile; opt into rotation with OBSCURA_ROTATE_PROFILE, or pin
one with OBSCURA_PROFILE=<index>.

Drive timezone from the process TZ so Intl.DateTimeFormat and Date report one
consistent zone. Default Europe/Berlin; override with OBSCURA_TIMEZONE.

Update navigator.webdriver test to expect false, matching real Chrome.

---------

Co-authored-by: SGavrl <[email protected]>
This commit is contained in:
johnnogueiraandSGavrl authored and GitHub committed 2026-06-08 21:23:50 +00:00
1 parent f20626bdbd
commit a23dad7cd9
7 files changed
+171 -26

No files matched your search

+14
View File
@@ -269,6 +269,20 @@ fn effective_v8_flags(user: Option<&str>) -> String {
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
// Pin the process timezone before V8/ICU reads it. V8 sources the zone for
// both Date (getTimezoneOffset, toString) and Intl.DateTimeFormat from TZ; left
// unset it defaults to UTC for Date while the page layer advertised a different
// zone, a cross-surface mismatch fingerprinting scripts flag. Default to
// Europe/Berlin; set OBSCURA_TIMEZONE to match the exit IP's region. An existing
// TZ from the host is respected.
// SAFETY: runs before any V8 isolate or worker thread starts, so the env is
// effectively single threaded here.
if let Some(tz) = std::env::var("OBSCURA_TIMEZONE").ok().filter(|s| !s.trim().is_empty()) {
unsafe { std::env::set_var("TZ", tz); }
} else if std::env::var_os("TZ").is_none() {
unsafe { std::env::set_var("TZ", "Europe/Berlin"); }
}
let quiet = is_quiet_command(&args.command);
let filter = select_log_filter(args.verbose, quiet);
tracing_subscriber::fmt()