mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-13 08:55:12 +02:00
fix(webrtc): ship the validated proxy realness config + CI guards
Audit follow-up (2026-06-10), all validated before commit. #2 WebRTC — the shipped baseline now MATCHES the manually-validated config (behind a residential proxy: host=<uuid>.local, srflx=proxy egress, No-Leak, gathering completes, indistinguishable from vanilla Firefox on BrowserLeaks + CreepJS): - prefs baseline obfuscate_host_addresses False->True; add zoom.stealth.webrtc.disable_ipv6=True; drop the dead media.peerconnection.ice.disableIPv6 (no-op on FF150) - launcher auto-derives the proxy egress IP via _geo.prepare_session_geo (one round-trip shared with the timezone resolution) and feeds nICEr via STEALTHFOX_WEBRTC_PUBLIC_IP + STEALTHFOX_WEBRTC_DISABLE_IPV6 in _build_env (sync + async); an explicit caller env still wins. The C++ mechanisms were already in firefox-9 — this activates them, no rebuild. #1 drop orphan prefs zoom.stealth.timezone + zoom.stealth.seed (read by no C++; the live ones are juggler.timezone.override + zoom.stealth.fpp.hw_seed). #3 release title 'rev N' instead of 'rev firefox-N'. CI guards (unit, leak-safe — no real proxy/creds, the kind that would have caught this gap at zero cost): - shipped-baseline guard + no-orphan-prefs (test_webrtc_realness.py) - egress auto-derive in _build_env (test_launcher_helpers.py) - prepare_session_geo returns (tz, egress) (test_geo.py) CI keeps faking 'behind a proxy' with an in-process TCP-only SOCKS5 + RFC 5737 TEST-NET IPs; real-proxy residential realness stays a LOCAL manual gate. 449 unit pass.
This commit is contained in:
parent
584ad97179
commit
e524695088
10 changed files with 249 additions and 51 deletions
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -377,12 +377,14 @@ jobs:
|
|||
TAG="${{ github.event.inputs.release_tag }}"
|
||||
[ -z "$TAG" ] && TAG="${GITHUB_REF_NAME}"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
# bare revision number for the release title: firefox-9 -> 9
|
||||
echo "num=${TAG#firefox-}" >> "$GITHUB_OUTPUT"
|
||||
echo "publishing DRAFT release for tag: $TAG"
|
||||
- name: Create DRAFT release with all assets
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: invisible_firefox (150.0.1) rev ${{ steps.tag.outputs.tag }}
|
||||
name: invisible_firefox (150.0.1) rev ${{ steps.tag.outputs.num }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
fail_on_unmatched_files: true
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ On failure:
|
|||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, NamedTuple, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
|
@ -136,22 +136,76 @@ def ip_to_timezone(ip: str, mmdb_path: Any) -> str:
|
|||
return tz
|
||||
|
||||
|
||||
class SessionGeo(NamedTuple):
|
||||
"""Geo facts resolved once per session from a single egress round-trip.
|
||||
|
||||
``timezone`` follows the precedence in the module docstring.
|
||||
``egress_ip`` is the proxy egress IP (the IP the *outside world* sees) when
|
||||
a proxy is set, else ``None`` — it feeds the WebRTC srflx override, which is
|
||||
only meaningful behind a proxy (a direct connection's real STUN already
|
||||
reports the truthful public IP, so we leave it alone).
|
||||
"""
|
||||
|
||||
timezone: str
|
||||
egress_ip: Optional[str]
|
||||
|
||||
|
||||
def prepare_session_geo(
|
||||
timezone: str, proxy: Optional[Dict[str, str]]
|
||||
) -> SessionGeo:
|
||||
"""Resolve the session timezone AND the proxy egress IP in ONE round-trip.
|
||||
|
||||
The egress IP is discovered once and reused for both the timezone mapping
|
||||
(when ``timezone`` is ``""``/``"auto"``) and the WebRTC public-IP override.
|
||||
Timezone precedence is identical to :func:`resolve_session_timezone`; the
|
||||
egress IP is best-effort for the WebRTC side (a discovery failure that the
|
||||
timezone path doesn't need won't break the launch — but if the timezone
|
||||
path *does* need it behind a proxy, that path still fails loudly).
|
||||
"""
|
||||
from .download import ensure_geoip_mmdb
|
||||
|
||||
tz = (timezone or "").strip()
|
||||
proxy_set = _proxy_is_set(proxy)
|
||||
|
||||
# One discovery, reused below. Behind a proxy we always want the egress IP
|
||||
# (for WebRTC) regardless of the timezone setting.
|
||||
egress_ip: Optional[str] = None
|
||||
egress_err: Optional[Exception] = None
|
||||
if proxy_set:
|
||||
try:
|
||||
egress_ip = discover_egress_ip(proxy)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
egress_err = exc
|
||||
|
||||
# Timezone resolution — same precedence as resolve_session_timezone.
|
||||
if tz and tz.lower() != "auto":
|
||||
return SessionGeo(tz, egress_ip) # explicit IANA wins
|
||||
try:
|
||||
ip = egress_ip if proxy_set else discover_egress_ip(None)
|
||||
if ip is None: # proxy set but discovery failed above
|
||||
raise egress_err or GeoTimezoneError("egress IP discovery failed")
|
||||
return SessionGeo(ip_to_timezone(ip, ensure_geoip_mmdb()), egress_ip)
|
||||
except Exception:
|
||||
if proxy_set:
|
||||
raise # fail-early behind a proxy (timezone_mismatch trap)
|
||||
return SessionGeo("", None) # no proxy: host TZ is a safe fallback
|
||||
|
||||
|
||||
def resolve_session_timezone(
|
||||
timezone: str, proxy: Optional[Dict[str, str]]
|
||||
) -> str:
|
||||
"""Map the user's ``timezone`` setting to a concrete IANA zone (or ``""``).
|
||||
|
||||
See the module docstring for the full precedence table. ``""``/``"auto"``
|
||||
ALWAYS resolve from the egress IP (proxy egress if a proxy is set, else the
|
||||
host's own public IP). On failure: with a proxy we raise
|
||||
:class:`GeoTimezoneError` (never silently use the host TZ behind a foreign
|
||||
proxy); without a proxy we fall back to ``""`` (host TZ) so a transient
|
||||
lookup failure can't break the launch.
|
||||
Timezone-only path (no WebRTC side effects): an explicit IANA zone wins and
|
||||
triggers NO network call; ``""``/``"auto"`` resolve from the egress IP. The
|
||||
launch path uses :func:`prepare_session_geo` instead (which additionally
|
||||
returns the egress IP for WebRTC); this standalone resolver is kept for
|
||||
third-party integrations that only want the zone. See the module docstring
|
||||
for the precedence table.
|
||||
"""
|
||||
tz = (timezone or "").strip()
|
||||
if tz and tz.lower() != "auto":
|
||||
return tz # explicit IANA wins
|
||||
# "" or "auto" → always resolve from the egress IP.
|
||||
return tz # explicit IANA wins — no egress lookup
|
||||
from .download import ensure_geoip_mmdb
|
||||
|
||||
proxy_set = _proxy_is_set(proxy)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from typing import Any, Dict, Optional, Union
|
|||
from playwright.async_api import Browser, BrowserContext, Playwright, async_playwright
|
||||
|
||||
from ._fpforge import Profile, generate_profile
|
||||
from ._geo import resolve_session_timezone
|
||||
from ._geo import prepare_session_geo
|
||||
from ._headless import make_virtual_display
|
||||
from ._proxy import configure_proxy as _configure_proxy_shared
|
||||
from .download import ensure_binary
|
||||
|
|
@ -73,16 +73,20 @@ class InvisiblePlaywright:
|
|||
self._browser: Optional[Browser] = None
|
||||
self._persistent_context: Optional[BrowserContext] = None
|
||||
self._virtual_display: Any = None
|
||||
# Proxy egress IP (WebRTC srflx override); discovered in __aenter__.
|
||||
self._webrtc_egress_ip: Optional[str] = None
|
||||
|
||||
async def __aenter__(self) -> Union[Browser, BrowserContext]:
|
||||
import sys as _sys
|
||||
# Resolve timezone="auto" (and the proxy-set-but-unset default) to a
|
||||
# concrete IANA zone before anything reads self._timezone. Run the
|
||||
# blocking geo lookup off the event loop. Fail-early if a proxy is set
|
||||
# but the egress zone can't be resolved.
|
||||
self._timezone = await asyncio.to_thread(
|
||||
resolve_session_timezone, self._timezone, self._proxy
|
||||
# Resolve timezone="auto" AND discover the proxy egress IP in one
|
||||
# round-trip, off the event loop, before anything reads self._timezone
|
||||
# or builds prefs/env. Fail-early if a proxy is set but the egress
|
||||
# can't be resolved.
|
||||
_geo = await asyncio.to_thread(
|
||||
prepare_session_geo, self._timezone, self._proxy
|
||||
)
|
||||
self._timezone = _geo.timezone
|
||||
self._webrtc_egress_ip = _geo.egress_ip
|
||||
executable = self._binary_path or ensure_binary()
|
||||
prefs = translate_profile_to_prefs(
|
||||
self._profile,
|
||||
|
|
@ -203,12 +207,16 @@ class InvisiblePlaywright:
|
|||
env = _os.environ.copy()
|
||||
if self._timezone:
|
||||
env["TZ"] = _tz_env(self._timezone)
|
||||
# Propagate STEALTHFOX_WEBRTC_PUBLIC_IP if the process set it — read
|
||||
# by nICEr's nr_stealth_bridge to inject a synthetic srflx candidate
|
||||
# matching the proxy egress IP. This avoids the StaticPref IPC
|
||||
# propagation timing issue between parent and socket processes.
|
||||
if _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP"):
|
||||
env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"]
|
||||
# WebRTC srflx override: feed nICEr's nr_stealth_bridge the proxy egress
|
||||
# IP (caller's explicit env var wins, else the IP auto-discovered in
|
||||
# __aenter__) and drop IPv6 from gathering behind a proxy.
|
||||
webrtc_ip = (
|
||||
_os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP")
|
||||
or self._webrtc_egress_ip
|
||||
)
|
||||
if webrtc_ip:
|
||||
env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = webrtc_ip
|
||||
env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] = "1"
|
||||
return env
|
||||
|
||||
def _resolve_headless(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from typing import Any, Dict, Optional, Union
|
|||
from playwright.sync_api import Browser, BrowserContext, Playwright, sync_playwright
|
||||
|
||||
from ._fpforge import Profile, generate_profile
|
||||
from ._geo import resolve_session_timezone
|
||||
from ._geo import prepare_session_geo
|
||||
from ._headless import make_virtual_display
|
||||
from ._proxy import configure_proxy as _configure_proxy_shared
|
||||
from .download import ensure_binary
|
||||
|
|
@ -183,12 +183,19 @@ class InvisiblePlaywright:
|
|||
self._browser: Optional[Browser] = None
|
||||
self._persistent_context: Optional[BrowserContext] = None
|
||||
self._virtual_display: Any = None
|
||||
# Proxy egress IP, discovered at launch (see __enter__). Feeds the
|
||||
# WebRTC srflx override so the candidate matches the proxy IP, not the
|
||||
# real host IP. None when no proxy is set.
|
||||
self._webrtc_egress_ip: Optional[str] = None
|
||||
|
||||
def __enter__(self) -> Union[Browser, BrowserContext]:
|
||||
# Resolve timezone="auto" (and the proxy-set-but-unset default) to a
|
||||
# concrete IANA zone before anything reads self._timezone. Fail-early
|
||||
# if a proxy is set but the egress zone can't be resolved.
|
||||
self._timezone = resolve_session_timezone(self._timezone, self._proxy)
|
||||
# concrete IANA zone AND discover the proxy egress IP — one round-trip,
|
||||
# before anything reads self._timezone or builds prefs/env. Fail-early
|
||||
# if a proxy is set but the egress can't be resolved.
|
||||
_geo = prepare_session_geo(self._timezone, self._proxy)
|
||||
self._timezone = _geo.timezone
|
||||
self._webrtc_egress_ip = _geo.egress_ip
|
||||
executable = self._binary_path or ensure_binary()
|
||||
prefs = self._build_prefs()
|
||||
playwright_proxy = _configure_proxy_shared(self._proxy, prefs)
|
||||
|
|
@ -354,12 +361,19 @@ class InvisiblePlaywright:
|
|||
env = _os.environ.copy()
|
||||
if self._timezone:
|
||||
env["TZ"] = _tz_env(self._timezone)
|
||||
# Propagate STEALTHFOX_WEBRTC_PUBLIC_IP if the process set it — read
|
||||
# by nICEr's nr_stealth_bridge to inject a synthetic srflx candidate
|
||||
# matching the proxy egress IP. This avoids the StaticPref IPC
|
||||
# propagation timing issue between parent and socket processes.
|
||||
if _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP"):
|
||||
env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"]
|
||||
# WebRTC srflx override: feed nICEr's nr_stealth_bridge the proxy egress
|
||||
# IP so the srflx candidate matches the proxy (not the real host the
|
||||
# UDP STUN would otherwise leak). An explicit env var set by the caller
|
||||
# wins; otherwise we use the egress IP auto-discovered in __enter__.
|
||||
# Behind a proxy we also drop IPv6 from gathering (the disableIPv6 pref
|
||||
# is dead on FF150 — the bridge filter is the real switch).
|
||||
webrtc_ip = (
|
||||
_os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP")
|
||||
or self._webrtc_egress_ip
|
||||
)
|
||||
if webrtc_ip:
|
||||
env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = webrtc_ip
|
||||
env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] = "1"
|
||||
return env
|
||||
|
||||
def _resolve_headless(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -208,15 +208,21 @@ _BASELINE: Dict[str, Any] = {
|
|||
"privacy.fingerprintingProtection.pbmode": False,
|
||||
"privacy.fingerprintingProtection.remoteOverrides.enabled": False,
|
||||
|
||||
# WebRTC: enabled, no public IP leak.
|
||||
# obfuscate_host_addresses=false: our C++ injection handles candidate
|
||||
# selection; mDNS causes mDNS-IPC to hang in sandboxed content processes.
|
||||
# disableIPv6=true keeps IPv6 out of gathering (less entropy, no IPv6 leak).
|
||||
# WebRTC: enabled, looks like a real Firefox behind NAT, no real-IP leak.
|
||||
# obfuscate_host_addresses=true → host candidate is `<uuid>.local` mDNS,
|
||||
# exactly like vanilla Firefox (BrowserLeaks "No Leak", Local IP "-").
|
||||
# The mDNS-IPC hang feared on older builds does NOT reproduce on FF150.
|
||||
# The proxy-egress srflx is injected by our C++ (srflx swap §17 + fallback
|
||||
# §17.B), fed the egress IP via STEALTHFOX_WEBRTC_PUBLIC_IP from
|
||||
# launcher._build_env (auto-discovered from the proxy).
|
||||
# IPv6: media.peerconnection.ice.disableIPv6 is DEAD on FF150 (read by no
|
||||
# ICE-gathering code). The real switch is our zoom.stealth.webrtc.disable_ipv6
|
||||
# (nICEr addrs.cpp filter) + the STEALTHFOX_WEBRTC_DISABLE_IPV6 env.
|
||||
"media.peerconnection.enabled": True,
|
||||
"media.peerconnection.ice.no_host": False,
|
||||
"media.peerconnection.ice.default_address_only": False,
|
||||
"media.peerconnection.ice.obfuscate_host_addresses": False,
|
||||
"media.peerconnection.ice.disableIPv6": True,
|
||||
"media.peerconnection.ice.obfuscate_host_addresses": True,
|
||||
"zoom.stealth.webrtc.disable_ipv6": True,
|
||||
"media.peerconnection.ice.proxy_only": False,
|
||||
"media.peerconnection.ice.relay_only": False,
|
||||
"media.peerconnection.use_document_iceservers": True,
|
||||
|
|
@ -551,12 +557,17 @@ def translate_profile_to_prefs(
|
|||
prefs["privacy.spoof_english"] = 0
|
||||
|
||||
if timezone:
|
||||
prefs["zoom.stealth.timezone"] = timezone
|
||||
# juggler.timezone.override is the SOLE source of truth read by the C++
|
||||
# timezone chain (BrowsingContext::Attach/DidSet, ContentChild). The old
|
||||
# zoom.stealth.timezone pref was declared in the yaml but read by NO
|
||||
# code — dropped here on 2026-06-10 (see 20-our-patches.md §8).
|
||||
prefs["juggler.timezone.override"] = timezone
|
||||
|
||||
# Cross-process seed (canvas noise + DWrite gamma share this).
|
||||
# Cross-process seed (canvas noise + DWrite gamma share this). Only
|
||||
# zoom.stealth.fpp.hw_seed is read by the C++; the old zoom.stealth.seed
|
||||
# alias was never declared in the yaml and read by nothing — dropped
|
||||
# 2026-06-10.
|
||||
prefs["zoom.stealth.fpp.hw_seed"] = profile.seed
|
||||
prefs["zoom.stealth.seed"] = profile.seed
|
||||
|
||||
# Synthetic host ICE candidate — injected by C++ when addr_ct==0 (SOCKS5
|
||||
# proxy suppresses all local addresses so Firefox can't gather host cands).
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from invisible_playwright._geo import (
|
|||
_proxy_is_set,
|
||||
discover_egress_ip,
|
||||
ip_to_timezone,
|
||||
prepare_session_geo,
|
||||
resolve_session_timezone,
|
||||
)
|
||||
|
||||
|
|
@ -286,3 +287,39 @@ def test_resolve_proxy_failure_raises(monkeypatch):
|
|||
resolve_session_timezone("auto", SOCKS)
|
||||
with pytest.raises(GeoTimezoneError):
|
||||
resolve_session_timezone("", SOCKS)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# prepare_session_geo — one round-trip for BOTH timezone + the WebRTC
|
||||
# egress IP. The egress feeds the srflx override (only behind a proxy).
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@pytest.mark.unit
|
||||
def test_prepare_geo_egress_present_behind_proxy(stub_egress):
|
||||
geo = prepare_session_geo("auto", SOCKS)
|
||||
assert geo.timezone == "America/New_York"
|
||||
assert geo.egress_ip == "203.0.113.7" # discovered for WebRTC
|
||||
assert stub_egress["proxy_arg"] == SOCKS
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_prepare_geo_egress_present_even_with_explicit_tz(stub_egress):
|
||||
# explicit IANA zone still needs the egress for WebRTC behind a proxy.
|
||||
geo = prepare_session_geo("Asia/Tokyo", SOCKS)
|
||||
assert geo.timezone == "Asia/Tokyo"
|
||||
assert geo.egress_ip == "203.0.113.7"
|
||||
assert stub_egress["called"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_prepare_geo_no_egress_without_proxy(stub_egress):
|
||||
# no proxy → no WebRTC override (real STUN already tells the truth).
|
||||
geo = prepare_session_geo("auto", None)
|
||||
assert geo.timezone == "America/New_York"
|
||||
assert geo.egress_ip is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_prepare_geo_timezone_matches_resolve_session_timezone(stub_egress):
|
||||
# the thin tz wrapper must stay equivalent to prepare_session_geo().timezone
|
||||
for tz, proxy in [("Asia/Tokyo", SOCKS), ("auto", HTTP), ("", None)]:
|
||||
assert prepare_session_geo(tz, proxy).timezone == resolve_session_timezone(tz, proxy)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ _REQUIRED_PREFS_KEYS = (
|
|||
"intl.accept_languages",
|
||||
"general.useragent.locale",
|
||||
"intl.locale.requested",
|
||||
"zoom.stealth.seed",
|
||||
"zoom.stealth.fpp.hw_seed",
|
||||
"zoom.stealth.webrtc.host_ip",
|
||||
"zoom.stealth.webgl.renderer",
|
||||
|
|
|
|||
|
|
@ -169,3 +169,38 @@ def test_default_context_includes_locale_when_set():
|
|||
def test_default_context_omits_locale_when_empty():
|
||||
ip = InvisiblePlaywright(seed=42, locale="")
|
||||
assert "locale" not in ip._default_context_kwargs()
|
||||
|
||||
|
||||
# ── InvisiblePlaywright._build_env — WebRTC egress auto-derive ─────────
|
||||
# Locks the 2026-06-10 fix: behind a proxy the launcher feeds the discovered
|
||||
# egress IP to nICEr (srflx override) + drops IPv6. Without it, a proxied
|
||||
# session's WebRTC silently fell back to leaking/blocking. Runs in tests.yml.
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_env_injects_webrtc_egress_when_discovered():
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = "203.0.113.9" # what __enter__ resolves behind a proxy
|
||||
env = ip._build_env()
|
||||
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "203.0.113.9"
|
||||
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_env_no_webrtc_keys_without_proxy(monkeypatch):
|
||||
monkeypatch.delenv("STEALTHFOX_WEBRTC_PUBLIC_IP", raising=False)
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = None # no proxy → real STUN already truthful
|
||||
env = ip._build_env()
|
||||
assert "STEALTHFOX_WEBRTC_PUBLIC_IP" not in env
|
||||
assert "STEALTHFOX_WEBRTC_DISABLE_IPV6" not in env
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_env_caller_env_override_wins(monkeypatch):
|
||||
monkeypatch.setenv("STEALTHFOX_WEBRTC_PUBLIC_IP", "198.51.100.5")
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = "203.0.113.9" # auto-discovered
|
||||
env = ip._build_env()
|
||||
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "198.51.100.5" # caller wins
|
||||
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"
|
||||
|
|
|
|||
|
|
@ -158,21 +158,22 @@ def test_webgl_extensions_cleared_on_windows(monkeypatch):
|
|||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_timezone_set_propagates_to_both_keys():
|
||||
# TZ1
|
||||
def test_timezone_set_uses_juggler_pref():
|
||||
# TZ1 — juggler.timezone.override is the sole C++-read timezone pref;
|
||||
# the old zoom.stealth.timezone alias (orphan) must NOT be reintroduced.
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p, timezone="America/New_York")
|
||||
assert prefs["zoom.stealth.timezone"] == "America/New_York"
|
||||
assert prefs["juggler.timezone.override"] == "America/New_York"
|
||||
assert "zoom.stealth.timezone" not in prefs
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_timezone_empty_omits_both_keys():
|
||||
def test_timezone_empty_omits_the_key():
|
||||
# TZ2
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p, timezone="")
|
||||
assert "zoom.stealth.timezone" not in prefs
|
||||
assert "juggler.timezone.override" not in prefs
|
||||
assert "zoom.stealth.timezone" not in prefs
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -200,10 +201,10 @@ def test_extra_prefs_none_value_deletes_key():
|
|||
|
||||
@pytest.mark.unit
|
||||
def test_extra_prefs_overrides_existing_key():
|
||||
# EP3
|
||||
# EP3 — override a real baseline key (hw_seed is the live cross-process seed)
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p, extra_prefs={"zoom.stealth.seed": 999})
|
||||
assert prefs["zoom.stealth.seed"] == 999
|
||||
prefs = translate_profile_to_prefs(p, extra_prefs={"zoom.stealth.fpp.hw_seed": 999})
|
||||
assert prefs["zoom.stealth.fpp.hw_seed"] == 999
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
|
|
|||
|
|
@ -214,6 +214,43 @@ def test_mdns_host_is_invisible_to_creep_resolver():
|
|||
assert creep_get_ipaddress("v=0\r\nc=IN IP4 0.0.0.0\r\n" f"a={HOST_MDNS}\r\n") is None
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# SHIPPED-BASELINE guard — the cheap unit test that would have caught the
|
||||
# 2026-06-10 gap (baseline obfuscate=False, dead disableIPv6, orphan prefs).
|
||||
# These lock the shipped wrapper config to the manually-validated one so a
|
||||
# future edit / merge can't silently un-ship it. Run in tests.yml.
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
from invisible_playwright._fpforge import generate_profile # noqa: E402
|
||||
from invisible_playwright.prefs import translate_profile_to_prefs # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_shipped_webrtc_baseline_is_the_validated_config():
|
||||
prefs = translate_profile_to_prefs(generate_profile(seed=42))
|
||||
# host candidate must be mDNS .local like vanilla Firefox (manually
|
||||
# validated on BrowserLeaks/CreepJS through a residential proxy) — not a
|
||||
# raw LAN IP.
|
||||
assert prefs["media.peerconnection.ice.obfuscate_host_addresses"] is True
|
||||
# IPv6 dropped via OUR live filter pref; the native pref is dead on FF150
|
||||
# and must not be relied upon (or re-introduced as if it worked).
|
||||
assert prefs["zoom.stealth.webrtc.disable_ipv6"] is True
|
||||
assert "media.peerconnection.ice.disableIPv6" not in prefs
|
||||
# peerconnection stays ON (a disabled WebRTC is itself a tell).
|
||||
assert prefs["media.peerconnection.enabled"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_orphan_prefs_in_baseline():
|
||||
"""zoom.stealth.timezone / zoom.stealth.seed are read by NO C++ — they must
|
||||
not be written (juggler.timezone.override + zoom.stealth.fpp.hw_seed are the
|
||||
real ones). Guards against re-introducing a pref the binary ignores."""
|
||||
prefs = translate_profile_to_prefs(generate_profile(seed=42), timezone="America/Chicago")
|
||||
assert "zoom.stealth.timezone" not in prefs
|
||||
assert "zoom.stealth.seed" not in prefs
|
||||
assert prefs["juggler.timezone.override"] == "America/Chicago"
|
||||
assert "zoom.stealth.fpp.hw_seed" in prefs
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Fake-proxy infrastructure for e2e: a tiny TCP-only SOCKS5 server.
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue