feat(stealth): shrink detectable surface — lazy Runtime.enable, native timezone/WebRTC, opt-in canvas noise
Borrow anti-detection hardening from Scrapling/patchright, preferring native CDP/Chrome overrides over JS lies: - Runtime.enable is now opt-in via AGENT_BROWSER_CAPTURE_CONSOLE (default off). It was called on every session INCLUDING CdpAttach (the user's real Chrome), leaking the patchright/rebrowser "runtime" CDP signal and undermining the "real browser, no lies" guarantee. Runtime.evaluate/callFunctionOn and runIfWaitingForDebugger work without it; only console/error capture needs it. The console/errors commands now return a hint when capture is disabled. - Timezone alignment via native Emulation.setTimezoneOverride, opt-in with AGENT_BROWSER_TIMEZONE=<IANA>|auto (FullLaunch only). Intl and Date both follow with no JS artifact. - WebRTC IP-leak handling via the --force-webrtc-ip-handling-policy Chrome flag: auto disable_non_proxied_udp when a proxy is set (so the real IP can't leak past the proxy); AGENT_BROWSER_BLOCK_WEBRTC=1 hides the local IP when there is no proxy; =0 opts out. - Opt-in canvas/audio fingerprint noise via AGENT_BROWSER_HIDE_CANVAS=1 (FullLaunch only). Session-stable seed so reads stay consistent within a session while differing from the headless-stable hash. Adds 5 unit tests; full suite 751 passed, 0 failed.
This commit is contained in:
@@ -305,6 +305,22 @@ pub struct BrowserManager {
|
||||
/// Origins visited during this session, used by save_state to collect cross-origin localStorage.
|
||||
visited_origins: HashSet<String>,
|
||||
next_tab_id: u32,
|
||||
/// Whether to enable the CDP `Runtime` domain (console / error / exception capture).
|
||||
/// OFF by default for stealth: a live `Runtime.enable` is a detectable CDP signal
|
||||
/// (the patchright / rebrowser "runtime leak") — even when attached to the user's
|
||||
/// real Chrome. Opt in via `AGENT_BROWSER_CAPTURE_CONSOLE=1` when you need the
|
||||
/// `console` / `errors` commands to return page output.
|
||||
pub capture_console: bool,
|
||||
}
|
||||
|
||||
/// Whether console/error capture (and thus `Runtime.enable`) is opted into for this
|
||||
/// daemon. Defaults to `false` so the common automation path leaves no Runtime-domain
|
||||
/// fingerprint. Set `AGENT_BROWSER_CAPTURE_CONSOLE=1` (or `true`) to turn it on.
|
||||
pub fn console_capture_enabled() -> bool {
|
||||
std::env::var("AGENT_BROWSER_CAPTURE_CONSOLE")
|
||||
.ok()
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
const LIGHTPANDA_CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
@@ -413,6 +429,7 @@ impl BrowserManager {
|
||||
ignore_https_errors,
|
||||
visited_origins: HashSet::new(),
|
||||
next_tab_id: 1,
|
||||
capture_console: console_capture_enabled(),
|
||||
};
|
||||
manager.discover_and_attach_targets().await?;
|
||||
manager
|
||||
@@ -502,6 +519,7 @@ impl BrowserManager {
|
||||
ignore_https_errors: false,
|
||||
visited_origins: HashSet::new(),
|
||||
next_tab_id: 1,
|
||||
capture_console: console_capture_enabled(),
|
||||
};
|
||||
|
||||
if direct_page {
|
||||
@@ -629,9 +647,14 @@ impl BrowserManager {
|
||||
self.client
|
||||
.send_command_no_params("Page.enable", Some(session_id))
|
||||
.await?;
|
||||
self.client
|
||||
.send_command_no_params("Runtime.enable", Some(session_id))
|
||||
.await?;
|
||||
// `Runtime.enable` leaves a detectable CDP signal (the patchright/rebrowser
|
||||
// "runtime leak"), so only enable it when console/error capture is opted in.
|
||||
// `Runtime.evaluate` / `Runtime.callFunctionOn` work fine without it.
|
||||
if self.capture_console {
|
||||
self.client
|
||||
.send_command_no_params("Runtime.enable", Some(session_id))
|
||||
.await?;
|
||||
}
|
||||
// Resume the target if it is paused waiting for the debugger.
|
||||
// This is needed for real browser sessions (Chrome 144+) where targets
|
||||
// are paused after attach until explicitly resumed. No-op otherwise.
|
||||
@@ -665,9 +688,12 @@ impl BrowserManager {
|
||||
self.client
|
||||
.send_command_no_params("Page.enable", None)
|
||||
.await?;
|
||||
self.client
|
||||
.send_command_no_params("Runtime.enable", None)
|
||||
.await?;
|
||||
// See `enable_domains`: `Runtime.enable` is a CDP fingerprint, gated on opt-in.
|
||||
if self.capture_console {
|
||||
self.client
|
||||
.send_command_no_params("Runtime.enable", None)
|
||||
.await?;
|
||||
}
|
||||
let _ = self
|
||||
.client
|
||||
.send_command_no_params("Runtime.runIfWaitingForDebugger", None)
|
||||
@@ -1659,6 +1685,7 @@ async fn initialize_lightpanda_manager(
|
||||
ignore_https_errors: false,
|
||||
visited_origins: HashSet::new(),
|
||||
next_tab_id: 1,
|
||||
capture_console: console_capture_enabled(),
|
||||
};
|
||||
|
||||
match discover_and_attach_lightpanda_targets(&mut manager, deadline).await {
|
||||
|
||||
Reference in New Issue
Block a user