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:
@@ -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"
|
||||
|
||||
+9
-8
@@ -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.
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user